changeset 16:7626b099aefd

More comments.
author Brad Greco <brad@bgreco.net>
date Tue, 30 Apr 2019 20:40:58 -0400
parents 23f2e0da1094
children 68d7834dc28e
files ServerMonitor/Controls/MatchComboBox.cs ServerMonitor/Forms/CheckForm.cs ServerMonitor/Forms/ServerSummaryForm.cs ServerMonitor/Forms/SettingsForm.cs ServerMonitor/Objects/Checks/Check.cs
diffstat 5 files changed, 308 insertions(+), 101 deletions(-) [+]
line wrap: on
line diff
--- a/ServerMonitor/Controls/MatchComboBox.cs	Mon Apr 22 21:11:27 2019 -0400
+++ b/ServerMonitor/Controls/MatchComboBox.cs	Tue Apr 30 20:40:58 2019 -0400
@@ -21,5 +21,19 @@
     }
 
     /// <summary>Types of matches that can be run against a response string.</summary>
-    public enum MatchType { Equals = 0, NotEquals = 1, Contains = 2, NotContains = 3, GreaterThan = 4, LessThan = 5 }
+    public enum MatchType
+    {
+        /// <summary>Indicates that the result string must equal the expected pattern.</summary>
+        Equals = 0,
+        /// <summary>Indicates that the result string must not equal the expected pattern.</summary>
+        NotEquals = 1,
+        /// <summary>Indicates that the result string must contain expected pattern.</summary>
+        Contains = 2,
+        /// <summary>Indicates that the result string must not contain expected pattern.</summary>
+        NotContains = 3,
+        /// <summary>Indicates that the result string must be numeric and greater than the expected numeric pattern.</summary>
+        GreaterThan = 4,
+        /// <summary>Indicates that the result string must be numeric and less than the expected numeric pattern.</summary>
+        LessThan = 5
+    }
 }
--- a/ServerMonitor/Forms/CheckForm.cs	Mon Apr 22 21:11:27 2019 -0400
+++ b/ServerMonitor/Forms/CheckForm.cs	Tue Apr 30 20:40:58 2019 -0400
@@ -250,6 +250,9 @@
             // again.
             CancellationTokenSource localCancellationTokenSource = new CancellationTokenSource();
             cancellationTokenSource = localCancellationTokenSource;
+            // Do not update the check status or execution history when the check is run interactively.
+            // No need to pollute the history with manual executions that will likely fail as a
+            // check is being configured.
             CheckResult result = await workCheck.ExecuteAsync(cancellationTokenSource.Token, false);
             if (!localCancellationTokenSource.IsCancellationRequested)
                 OnRunFinished(result);
--- a/ServerMonitor/Forms/ServerSummaryForm.cs	Mon Apr 22 21:11:27 2019 -0400
+++ b/ServerMonitor/Forms/ServerSummaryForm.cs	Tue Apr 30 20:40:58 2019 -0400
@@ -1,6 +1,5 @@
 using NAppUpdate.Framework;
 using NAppUpdate.Framework.Sources;
-using NAppUpdate.Framework.Tasks;
 using ServerMonitorApp.Properties;
 using System;
 using System.Collections.Generic;
@@ -8,11 +7,11 @@
 using System.Data;
 using System.Drawing;
 using System.Linq;
-using System.Text;
 using System.Windows.Forms;
 
 namespace ServerMonitorApp
 {
+    /// <summary>Main application form that shows an overview of all servers.</summary>
     public partial class ServerSummaryForm : Form
     {
         private readonly Dictionary<Server, ServerForm> serverForms = new Dictionary<Server, ServerForm>();
@@ -23,17 +22,135 @@
             InitializeComponent();
         }
 
+        private void ServerSummaryForm_Load(object sender, EventArgs e)
+        {
+            // Restore the window size from the previous session.
+            Size size = Settings.Default.SummaryFormSize;
+            if (size.Height > 0 && size.Width > 0)
+                Size = size;
+
+            // Resize the images in buttons to fit the button size.
+            Helpers.FormatImageButton(NewServerButton);
+            Helpers.FormatImageButton(SettingsButton);
+            // Create the global server monitor object.
+            monitor = new ServerMonitor(this);
+            // Load the server configuration file.
+            while (true)
+            {
+                try
+                {
+                    // If the configuration file is loaded successfully, proceed.
+                    monitor.LoadServers();
+                    break;
+                }
+                catch (Exception ex)
+                {
+                    // If there was an error loading the config file, show it.
+                    DialogResult result = MessageBox.Show("Could not load servers. Please fix or delete the file " + monitor.ConfigFile + Environment.NewLine + Environment.NewLine
+                        + "Error details:" + Environment.NewLine + ex.GetAllMessages(),
+                        "Error loading servers", MessageBoxButtons.RetryCancel, MessageBoxIcon.Error);
+                    // If the error message was cancelled, exit. Otherwise, retry by continuing the loop.
+                    if (result == DialogResult.Cancel)
+                    {
+                        Environment.Exit(1);
+                    }
+                }
+            }
+            // Listen to server monitor events.
+            monitor.CheckStatusChanged += Monitor_CheckStatusChanged;
+            // Show the servers.
+            RefreshDisplay();
+            // If any servers have encrypted private keys, attempt to open them immediately
+            // rather than interrupting the user later when they are first used.
+            CollectPrivateKeyPasswords();
+            CheckForUpdate();
+        }
+
+        /// <summary>Shows a form to edit or create a server.</summary>
+        /// <param name="server">The server to edit. If null, a new server will be created.</param>
+        /// <param name="activate">Whether the server form should be activated.</param>
+        /// <returns>The created or existing server for for the server.</returns>
+        private ServerForm ShowServerForm(Server server, bool activate = true)
+        {
+            bool isNewServer = false;
+            if (server == null)
+            {
+                // Create a new server if none was given.
+                server = new Server();
+                // The server is added to the server monitor immediately so that
+                // checks can be created and run. If the server was created by
+                // mistake, it will automatically be removed when the form is closed
+                // as long as no information was entered into the form.
+                monitor.AddServer(server);
+                isNewServer = true;
+            }
+            if (serverForms.TryGetValue(server, out ServerForm form))
+            {
+                // If the server form is already open, just activate it if requested.
+                if (activate)
+                    form.Activate();
+            }
+            else
+            {
+                // Open a new server form for the server.
+                form = new ServerForm(monitor, server, isNewServer);
+                // Keep track of the form so it can be activated later if the server is clicked again.
+                serverForms[server] = form;
+                form.FormClosing += ServerForm_FormClosing;
+                form.Show(activate);
+            }
+            return form;
+        }
+
+        /// <summary>Refreshes the server list with the server monitor data.</summary>
+        private void RefreshDisplay()
+        {
+            // Delete all server controls and recreate them.
+            ServerPanel.Controls.Clear();
+            foreach (Server server in monitor.Servers)
+            {
+                // Subscribe to server events.
+                server.EnabledChanged -= Server_EnabledChanged;
+                server.EnabledChanged += Server_EnabledChanged;
+                // Create a server control and add it to the panel.
+                ServerSummaryControl control = new ServerSummaryControl(server);
+                control.ContextMenuStrip = ServerContextMenu;
+                control.Click += ServerSummaryControl_Click;
+                ServerPanel.Controls.Add(control);
+            }
+            // Refresh the form icon that depends on the status of all servers.
+            UpdateIcon();
+        }
+
+        /// <summary>Refreshes a single server control.</summary>
+        /// <param name="server">The server to refresh.</param>
+        private void RefreshServer(Server server)
+        {
+            ServerPanel.Controls.Cast<ServerSummaryControl>().FirstOrDefault(c => c.Server == server).Refresh();
+            // The server's status might have changed, so refresh the form icon.
+            UpdateIcon();
+        }
+
+        /// <summary>Flashes the taskbar button for a server form.</summary>
+        /// <param name="check">The check that needs attention.</param>
         public void AlertServerForm(Check check)
         {
-            bool existingForm = serverForms.ContainsKey(check.Server);
+            // Show the form, but don't activate it since the user did not initiate this event.
             ServerForm form = ShowServerForm(check.Server, false);
+            // Flash the taskbar button.
             Win32Helpers.FlashWindowEx(form);
-            if (!existingForm)
+            // If the form was not already open, focus the Log tab and display
+            // only the log entries for this check. Do not do this if the form
+            // was already open since the user might be in the middle of doing
+            // something with it.
+            if (!serverForms.ContainsKey(check.Server))
             {
                 form.ShowLog(check);
             }
         }
 
+        /// <summary>Shows a balloon popup with the results of a failed check.</summary>
+        /// <param name="check">The check that failed.</param>
         public void ShowBalloon(CheckResult result)
         {
             string title = string.Format("{0}: {1} failed", result.Check.Server.Name, result.Check.Name);
@@ -41,86 +158,16 @@
             NotifyIcon.ShowBalloonTip(30000, title, result.Message, GetToolTipIcon(result.CheckStatus));
         }
 
-        private void ServerSummaryForm_Load(object sender, EventArgs e)
-        {
-            Size size = Settings.Default.SummaryFormSize;
-            if (size.Height > 0 && size.Width > 0)
-                Size = size;
-
-            Helpers.FormatImageButton(NewServerButton);
-            Helpers.FormatImageButton(SettingsButton);
-            monitor = new ServerMonitor(this);
-            while (true)
-            {
-                try
-                {
-                    monitor.LoadServers();
-                    break;
-                }
-                catch (Exception ex)
-                {
-                    DialogResult result = MessageBox.Show("Could not load servers. Please fix or delete the file " + monitor.ConfigFile + Environment.NewLine + Environment.NewLine
-                        + "Error details:" + Environment.NewLine + ex.GetAllMessages(),
-                        "Error loading servers", MessageBoxButtons.RetryCancel, MessageBoxIcon.Error);
-                    if (result == DialogResult.Cancel)
-                    {
-                        Environment.Exit(1);
-                    }
-                }
-            }
-            monitor.CheckStatusChanged += Monitor_CheckStatusChanged;
-            RefreshDisplay();
-            CollectPrivateKeyPasswords();
-            CheckForUpdate();
-        }
-
-        private ServerForm ShowServerForm(Server server, bool activate = true)
-        {
-            bool isNewServer = false;
-            if (server == null)
-            {
-                server = new Server();
-                monitor.AddServer(server);
-                isNewServer = true;
-            }
-            if (serverForms.TryGetValue(server, out ServerForm form))
-            {
-                if (activate)
-                    form.Activate();
-            }
-            else
-            {
-                form = new ServerForm(monitor, server, isNewServer);
-                serverForms[server] = form;
-                form.FormClosing += ServerForm_FormClosing;
-                form.Show(activate);
-            }
-            return form;
-        }
-
-        private void RefreshDisplay()
-        {
-            ServerPanel.Controls.Clear();
-            foreach (Server server in monitor.Servers)
-            {
-                server.EnabledChanged -= Server_EnabledChanged;
-                server.EnabledChanged += Server_EnabledChanged;
-                ServerSummaryControl control = new ServerSummaryControl(server);
-                control.ContextMenuStrip = ServerContextMenu;
-                control.Click += ServerSummaryControl_Click;
-                ServerPanel.Controls.Add(control);
-            }
-            UpdateIcon();
-        }
-
-        private void RefreshServer(Server server)
-        {
-            ServerPanel.Controls.Cast<ServerSummaryControl>().FirstOrDefault(c => c.Server == server).Refresh();
-            UpdateIcon();
-        }
-
+        /// <summary>Updates the form icon to reflect a summary of the status of all servers.</summary>
         private void UpdateIcon()
         {
+            // The status for the summary icon is the most severe status of all servers.
+            // When performing the comparison:
+            //  - Enabled servers use their current status.
+            //  - If a server is disabled due to a locked private key, report a warning.
+            //    Otherwise, report success to effectively ignore it.
+            // The integer value of the CheckStatus enum increases with the severity,
+            // so the maximum value of all servers gives the most severe status.
             CheckStatus status = monitor.Servers
                 .Select(s => s.Enabled
                     ? s.Status
@@ -131,23 +178,31 @@
             NotifyIcon.Icon = Icon;
         }
 
+        /// <summary>Prompts the user for the passwords to open all encrypted private keys.</summary>
         private void CollectPrivateKeyPasswords()
         {
+            // List of paths to keyfiles.
             List<string> triedKeys = new List<string>();
             foreach (Server server in monitor.Servers)
             {
+                // If the same private key is used for multiple servers, don't prompt
+                // the user multiple times to open the same keyfile.
                 if (triedKeys.Contains(server.KeyFile))
                     continue;
+                // Show the prompt.
                 ServerForm.OpenPrivateKey(monitor, server, this);
+                // Keep track of the keyfile so we don't needlessly ask again.
                 triedKeys.Add(server.KeyFile);
             }
         }
 
+        /// <summary>Refreshes a server control when the server state changes.</summary>
         private void Server_EnabledChanged(object sender, EventArgs e)
         {
             RefreshServer((Server)sender);
         }
 
+        /// <summary>Refreshes a server control when the server status might have changed.</summary>
         private void Monitor_CheckStatusChanged(object sender, CheckStatusChangedEventArgs e)
         {
             if (e.CheckResult != null)
@@ -156,6 +211,8 @@
             }
         }
 
+        /// <summary>Gets a Windows tooltip icon based on the severity of the message.</summary>
+        /// <param name="status">The status of the check that will be reported in the balloon tip.</param>
         private ToolTipIcon GetToolTipIcon(CheckStatus status)
         {
             switch (status)
@@ -167,17 +224,23 @@
             }
         }
 
+        /// <summary>Shows a server form when a server control is clicked.</summary>
         private void ServerSummaryControl_Click(object sender, EventArgs e)
         {
             ShowServerForm(((ServerSummaryControl)sender).Server);
         }
 
+        /// <summary>Handles the closing of a server form.</summary>
         private void ServerForm_FormClosing(object sender, FormClosingEventArgs e)
         {
             ServerForm form = (ServerForm)sender;
             form.FormClosing -= ServerForm_FormClosing;
             Server server = form.Server;
+            // Remove the closed form from the list of open forms.
             serverForms.Remove(form.Server);
+            // If there is no user data associated with the server, it can be deleted.
+            // This usually happens when the New Server button is clicked and the server form
+            // is closed without entering any information.
             if (server.IsEmpty())
             {
                 monitor.DeleteServer(server);
@@ -185,11 +248,14 @@
             RefreshDisplay();
         }
 
+        /// <summary>Shows a server form to create a new server.</summary>
         private void NewServerButton_Click(object sender, EventArgs e)
         {
             ShowServerForm(null);
         }
 
+        /// <summary>Hides the main form instead of exiting the application based on user preferences.</summary>
+        /// <remarks>Allows the monitor to run in the background without being shown in the taskbar.</remarks>
         private void ServerSummaryForm_FormClosing(object sender, FormClosingEventArgs e)
         {
             if ((e.CloseReason == CloseReason.None || e.CloseReason == CloseReason.UserClosing) && Settings.Default.HideToNotificationArea)
@@ -199,11 +265,13 @@
             }
         }
 
+        /// <summary>Shows the settings form.</summary>
         private void SettingsButton_Click(object sender, EventArgs e)
         {
             new SettingsForm().Show();
         }
 
+        /// <summary>Shows the details of a failed check when the balloon notification is clicked.</summary>
         private void NotifyIcon_BalloonTipClicked(object sender, EventArgs e)
         {
             CheckResult result = (CheckResult)(sender as NotifyIcon).Tag;
@@ -212,17 +280,21 @@
             form.WindowState = FormWindowState.Normal;
         }
 
+        /// <summary>Handles the server context menu.</summary>
         private void ServerContextMenu_ItemClicked(object sender, ToolStripItemClickedEventArgs e)
         {
             Server server = GetClickedServer((ContextMenuStrip)e.ClickedItem.Owner);
             if (e.ClickedItem == DeleteServerMenuItem)
             {
+                // Close the menu immediately so it doesn't stay open while the messagebox is shown.
                 ServerContextMenu.Close();
+                // Show the server delete confirmation dialog. No option to not ask again
+                // since it's a rare and very destructive event.
                 DialogResult result = MessageBox.Show(
                     string.Format("The server \"{0}\" and its {1} {2} will be deleted.", server.Name, server.Checks.Count, server.Checks.Count == 1 ? "check" : "checks"),
                     "Delete server",
                     MessageBoxButtons.OKCancel,
-                    MessageBoxIcon.Warning  );
+                    MessageBoxIcon.Warning);
                 if (result == DialogResult.OK)
                 {
                     monitor.DeleteServer(server);
@@ -234,7 +306,10 @@
                 bool enable = ToggleEnableServerMenuItem.Text == "Enable";
                 if (enable)
                 {
+                    // Close the menu immediately so it doesn't stay open while the messagebox is shown.
                     ServerContextMenu.Close();
+                    // Attempt to open the private key for the server immediately since it has not
+                    // been opened yet.
                     ServerForm.OpenPrivateKey(monitor, server, this);
                 }
                 server.Enabled = enable;
@@ -242,23 +317,31 @@
             }
         }
 
+        /// <summary>Activates the appropriate Enable/Disable menu option based on the server's current state.</summary>
         private void ServerContextMenu_Opening(object sender, CancelEventArgs e)
         {
             Server server = GetClickedServer((ContextMenuStrip)sender);
             ToggleEnableServerMenuItem.Text = server.Enabled ? "Disable" : "Enable";
         }
 
+        /// <summary>Gets the server corresponding to a server context menu.</summary>
         private Server GetClickedServer(ContextMenuStrip menu)
         {
             return ((ServerSummaryControl)menu.SourceControl).Server;
         }
 
+        /// <summary>Saves the window size after it is resized so it can be restored next time the program is run.</summary>
         private void ServerSummaryForm_ResizeEnd(object sender, EventArgs e)
         {
             Settings.Default.SummaryFormSize = Size;
             Settings.Default.Save();
         }
 
+        /// <summary>Shows the main form when the WM_SHOWMONITOR message is received.</summary>
+        /// <remarks>
+        /// This is used to make this a single-instance application. When a second copy of the program
+        /// is launched, it sends this message to activate the first copy and then exits.
+        /// </remarks>
         protected override void WndProc(ref Message m)
         {
             if (m.Msg == Win32Helpers.WM_SHOWMONITOR)
@@ -266,6 +349,7 @@
             base.WndProc(ref m);
         }
 
+        /// <summary>Handles clicks on the notification area icon.</summary>
         private void NotifyIcon_MouseClick(object sender, MouseEventArgs e)
         {
             if (e.Button == MouseButtons.Left)
@@ -274,16 +358,20 @@
                 NotificationIconMenu.Show();
         }
 
+        /// <summary>Shows the window.</summary>
         private void ShowWindow()
         {
             if (WindowState == FormWindowState.Minimized)
                 WindowState = FormWindowState.Normal;
+            // Do various things to try to get this window on top.
+            // We only do this as a result of user input, so it's ok. ;)
             Show();
             TopMost = true;
             TopMost = false;
             Activate();
         }
 
+        /// <summary>Handles clicks on the notification icon menu.</summary>
         private void NotificationIconMenu_ItemClicked(object sender, ToolStripItemClickedEventArgs e)
         {
             if (e.ClickedItem == ShowServerMonitorMenuItem)
@@ -292,27 +380,32 @@
                 Application.Exit();
         }
 
+        /// <summary>Begins checking for program updates in the background.</summary>
         private void CheckForUpdate()
         {
-            //System.Threading.Thread.Sleep(5000);
             UpdateManager manager = UpdateManager.Instance;
+            // Make the update manager happy if the program was just restarted to apply an update.
             manager.ReinstateIfRestarted();
             manager.UpdateSource = new SimpleWebSource("https://www.bgreco.net/test/servermonitor.xml");
             if (manager.State == UpdateManager.UpdateProcessState.NotChecked)
                 manager.BeginCheckForUpdates(CheckForUpdatesCallback, null);
         }
 
+        /// <summary>Callback after the program update check completes.</summary>
         private void CheckForUpdatesCallback(IAsyncResult result)
         {
             UpdateManager manager = UpdateManager.Instance;
             if (manager.UpdatesAvailable > 0)
             {
+                // Extract the new version number from the result.
                 GetUpdateInfo(out string version, out string _);
+                // If the user has not chosen to ignore this update, show a notification.
                 if (Settings.Default.IgnoreUpdate != version)
                     Invoke((MethodInvoker)(() => UpdatePanel.Show()));
             }
         }
 
+        /// <summary>Applies the program updates.</summary>
         private void PrepareUpdatesCallback(IAsyncResult result)
         {
             UpdateManager manager = UpdateManager.Instance;
@@ -320,8 +413,10 @@
             manager.ApplyUpdates(true);
         }
 
+        /// <summary>Shows information about a program update when the notification is clicked.</summary>
         private void UpdateLabel_Click(object sender, EventArgs e)
         {
+            // Extract the update information from the update manager result.
             GetUpdateInfo(out string version, out string changeMessage);
             string message = "Server Monitor version {0} is available for download." + Environment.NewLine
                 + Environment.NewLine
@@ -332,20 +427,25 @@
             using (UpdateDialog dialog = new UpdateDialog { Message = string.Format(message, version, changeMessage) })
             {
                 DialogResult result = dialog.ShowDialog();
+                // If the user declined the update and asked not to be notified again,
+                // save the preference so they will not be asked again for this version.
                 if (dialog.Checked && result == DialogResult.Cancel)
                 {
                     Settings.Default.IgnoreUpdate = version;
                     Settings.Default.Save();
                     UpdatePanel.Hide();
                 }
+                // If Yes was not chosen, do not apply the update.
                 if (result != DialogResult.OK)
                     return;
             }
             UpdateManager.Instance.BeginPrepareUpdates(PrepareUpdatesCallback, null);
         }
 
+        /// <summary>Extracts the update information from the update manager result.</summary>
         private void GetUpdateInfo(out string version, out string changeMessage)
         {
+            // The update description is in the form {version}:{change message}.
             string[] parts = UpdateManager.Instance.Tasks.First().Description.Split(new char[] { ':' }, 2);
             version = parts[0];
             changeMessage = parts[1];
--- a/ServerMonitor/Forms/SettingsForm.cs	Mon Apr 22 21:11:27 2019 -0400
+++ b/ServerMonitor/Forms/SettingsForm.cs	Tue Apr 30 20:40:58 2019 -0400
@@ -1,18 +1,12 @@
 using Microsoft.Win32;
 using ServerMonitorApp.Properties;
 using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Data;
-using System.Drawing;
-using System.Linq;
-using System.Text;
 using System.Text.RegularExpressions;
-using System.Threading.Tasks;
 using System.Windows.Forms;
 
 namespace ServerMonitorApp
 {
+    /// <summary>Application settings form.</summary>
     public partial class SettingsForm : Form
     {
         private readonly string autorunKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run";
@@ -26,11 +20,13 @@
         private void SettingsForm_Load(object sender, EventArgs e)
         {
             Icon = Resources.icon;
+            // Populate each action combo box with the available actions to perform on failure.
             foreach (ComboBox comboBox in new object[] { ErrorComboBox, WarningComboBox, InformationComboBox })
             {
                 comboBox.DataSource = Enum.GetValues(typeof(FailAction));
                 comboBox.Format += FailActionComboBox_Format;
             }
+            // Initialize controls with current settings.
             AutorunCheckBox.Checked = GetAutorun();
             KeepRunningCheckBox.Checked = Settings.Default.HideToNotificationArea;
             KeepLogDaysInput.Value = Settings.Default.KeepLogDays;
@@ -39,6 +35,8 @@
             InformationComboBox.SelectedItem = Settings.Default.InformationAction;
         }
 
+        /// <summary>Gets whether an autorun registry key for this program exists.</summary>
+        /// <returns>Whether an autorun registry key for this program exists</returns>
         private bool GetAutorun()
         {
             RegistryKey key = Registry.CurrentUser.OpenSubKey(autorunKey, false);
@@ -46,6 +44,8 @@
             return value.StartsWith(Application.ExecutablePath);
         }
 
+        /// <summary>Sets whether this program should automatically start with Windows.</summary>
+        /// <param name="autorun">Whether autorun should be enabled or disabled.</param>
         private void SetAutorun(bool autorun)
         {
             RegistryKey key = Registry.CurrentUser.OpenSubKey(autorunKey, true);
@@ -55,11 +55,15 @@
                 key.DeleteValue(autorunName, false);
         }
 
+        /// <summary>Shows a human readable description of possible check failure actions in the combo box.</summary>
         private void FailActionComboBox_Format(object sender, ListControlConvertEventArgs e)
         {
+            // Transform the "CamelCase" enum name in to a "Camel case" name, adding
+            // spaces and making all characters besides the first lower case.
             e.Value = e.Value.ToString().Substring(0, 1) + Regex.Replace(e.Value.ToString(), "(\\B[A-Z])", " $1").ToLower().Substring(1);
         }
 
+        /// <summary>Saves the user settings and closes the form.</summary>
         private void OkButton_Click(object sender, EventArgs e)
         {
             Settings.Default.HideToNotificationArea = KeepRunningCheckBox.Checked;
@@ -72,6 +76,7 @@
             Close();
         }
 
+        /// <summary>Closes the form without saving settings.</summary>
         private void CancelSettingsButton_Click(object sender, EventArgs e)
         {
             Close();
--- a/ServerMonitor/Objects/Checks/Check.cs	Mon Apr 22 21:11:27 2019 -0400
+++ b/ServerMonitor/Objects/Checks/Check.cs	Tue Apr 30 20:40:58 2019 -0400
@@ -8,11 +8,11 @@
 
 namespace ServerMonitorApp
 {
-    /*public enum CheckType
-    {
-        Command
-    }*/
-
+    /// <summary>The possible statuses of a check.</summary>
+    /// <remarks>
+    /// The integer values of the "completed" statuses (Success, Information,
+    /// Warning, Error) are in ascending order of severity.
+    /// </remarks>
     public enum CheckStatus
     {
         Success,
@@ -23,6 +23,7 @@
         Disabled,
     }
 
+    /// <summary>Base class for checks that run against a server and return a result status.</summary>
     public abstract class Check
     {
         private static Type[] _checkTypes;
@@ -37,60 +38,91 @@
             }
         }
 
+        /// <summary>The internal ID of the check.</summary>
         public int Id { get; set; }
 
+        /// <summary>The display name check.</summary>
         public string Name { get; set; }
 
-        /*public CheckType Type { get; set; }*/
-
+        /// <summary>The number of milliseconds the check must complete in before reporting failure.</summary>
         public int Timeout { get; set; }
 
+        /// <summary>Whether the check will be executed on a schedule.</summary>
         public bool Enabled { get; set; }
 
+        /// <summary>The schedule when the check will be executed.</summary>
         public Schedule Schedule { get; set; }
 
+        /// <summary>The date and time the check was last executed.</summary>
         public DateTime LastRunTime { get; set; }
 
+        /// <summary>The date and time the check was last executed on its schedule.</summary>
         public DateTime LastScheduledRunTime { get; set; }
 
+        /// <summary>The date and time the check is currently scheduled to execute next.</summary>
         public DateTime NextRunTime { get; set; }
 
+        /// <summary>The text output of the last execution of the check.</summary>
         public string LastMessage { get; set; }
 
+        /// <summary>The current status of the check.</summary>
         public CheckStatus Status { get; set; }
 
+        /// <summary>The status of the last check execution.</summary>
         public CheckStatus LastRunStatus { get; set; }
 
+        /// <summary>The severity level reported when the check execution fails.</summary>
         public CheckStatus FailStatus { get; set; }
 
+        /// <summary>The number of consecutive failed executions before the check begins reporting failure.</summary>
         public int MaxConsecutiveFailures { get; set; }
 
+        /// <summary>The current number of consecutive times the check has failed.</summary>
         [XmlIgnore]
         public int ConsecutiveFailures { get; set; }
 
+        /// <summary>The server the check belongs to.</summary>
         [XmlIgnore]
         public Server Server { get; set; }
 
+        /// <summary>Check constructor.</summary>
         public Check()
         {
+            // Set the default failure severity to Error.
             FailStatus = CheckStatus.Error;
         }
 
+        /// <summary>Displays the check name.</summary>
         public override string ToString()
         {
             return Name;
         }
 
+        /// <summary>Validates the check.</summary>
+        /// <param name="saving">
+        /// Whether the check is being saved.
+        /// Checks are validated before being saved and before being executed. This parameter allows
+        /// them to distinguish between these cases.
+        /// </param>
+        /// <returns>An empty string if the check is valid, or the reason the check is invalid.</returns>
         public virtual string Validate(bool saving = true)
         {
             string message = string.Empty;
+            // Allow blank names if the check is being executed before saving.
+            // This lets the user create a check and tinker with it without
+            // needing to type a name unless they want to save it.
             if (Name.IsNullOrEmpty() && saving)
                 message += "Name cannot be blank." + Environment.NewLine;
             return message;
         }
 
+        /// <summary>Executes the check asynchronously.</summary>
+        /// <param name="token">A token for cancelling the execution.</param>
+        /// <param name="update">Whether the check status and last execution time should be updated when the check completes.</param>
+        /// <returns>The result of the check execution.</returns>
         public async Task<CheckResult> ExecuteAsync(CancellationToken token = default(CancellationToken), bool update = true)
         {
+            // Do nothing if the check has already been cancelled.
             if (token.IsCancellationRequested)
                 return null;
 
@@ -98,30 +130,35 @@
             DateTime startTime = DateTime.Now;
             try
             {
+                // Execute the check.
                 Task<CheckResult> checkTask = ExecuteCheckAsync(token);
                 try
                 {
+                    // Wait for the check to complete or timeout, whichever happens first.
                     if (await Task.WhenAny(checkTask, Task.Delay(Timeout, token)) == checkTask)
                     {
+                        // If the check completed before timing out, retrieve the result.
                         result = await checkTask;
                     }
                     else
                     {
+                        // If the check timed out before completing, report failure.
                         result = Fail("Timed out.");
                     }
                 }
                 catch (TaskCanceledException)
                 {
+                    // If the check was cancelled, do not return a result so it will not be logged.
                     return null;
                 }
             }
             catch (Exception e)
             {
+                // If the execution threw an exception, report the exception as a failure.
                 result = Fail(e.GetBaseException().Message);
             }
             result.StartTime = startTime;
             result.EndTime = DateTime.Now;
-            // If a check is executed from the CheckForm, we don't want to update the status or log the event.
             if (update)
             {
                 Status = result.CheckStatus;
@@ -132,21 +169,36 @@
             return result;
         }
 
+        /// <summary>Generates a successful check result.</summary>
+        /// <param name="message">The execution result message.</param>
+        /// <returns>A successful check result.</returns>
         public CheckResult Pass(string message)
         {
             return new CheckResult(this, CheckStatus.Success, message);
         }
 
+        /// <summary>Generates a failed check result.</summary>
+        /// <param name="message">The execution result message.</param>
+        /// <returns>A failed check result.</returns>
         public CheckResult Fail(string message)
         {
+            // The severity is controlled by the check's FailStatus setting.
             return new CheckResult(this, FailStatus, message);
         }
 
+        /// <summary>Generates a failed check result from an exception.</summary>
+        /// <param name="e">The exception that caused the failure.</param>
+        /// <returns>A failed check result.</returns>
         protected CheckResult Fail(Exception e)
         {
-            return new CheckResult(this, FailStatus, e.GetBaseException().Message);
+            return Fail(e.GetBaseException().Message);
         }
 
+        /// <summary>Generates a check result by comparing integer values for equality.</summary>
+        /// <param name="expectedValue">The expected result value.</param>
+        /// <param name="resultValue">The actual result value generated by the check execution.</param>
+        /// <param name="description">Description of what the integer represents to use in the check result message. Example: "Exit code".</param>
+        /// <returns>A successful check result if the values are equal, or a failed check result if they are unequal.</returns>
         protected CheckResult GetIntResult(int expectedValue, int resultValue, string description)
         {
             if (expectedValue == resultValue)
@@ -155,11 +207,21 @@
                 return Fail(string.Format("{0}: {1} (expected: {2})", description, resultValue, expectedValue));
         }
 
+        /// <summary>Generates a check result by comparing string values.</summary>
+        /// <param name="matchType">The comparison that will be used on the strings.</param>
+        /// <param name="expectedPattern">The expected pattern to test the result against.</param>
+        /// <param name="useRegex">Whether the expected pattern should be treated as a regular expression.</param>
+        /// <param name="resultValue">The actual result value generated by the check execution.</param>
+        /// <param name="description">Description of what the string represents to use in the check result message.</param>
+        /// <returns>A successful check result if the string comparison succeeds, or a failed check result if it fails.</returns>
         protected CheckResult GetStringResult(MatchType matchType, string expectedPattern, bool useRegex, string resultValue, string description)
         {
             bool match;
             if (useRegex)
             {
+                // If the match type is equals or not equals, modify the regex by
+                // adding beginning and ending anchors if not already present
+                // to prevent partial matches.
                 if (matchType.In(MatchType.Equals, MatchType.NotEquals))
                 {
                     if (!expectedPattern.StartsWith("^"))
@@ -167,11 +229,13 @@
                     if (!expectedPattern.EndsWith("$"))
                         expectedPattern += "$";
                 }
+                // Execute the regex.
                 Regex re = new Regex(expectedPattern, RegexOptions.Singleline);
                 match = re.IsMatch(resultValue);
             }
             else
             {
+                // Simple string comparisons.
                 if (matchType.In(MatchType.Equals, MatchType.NotEquals))
                 {
                     match = expectedPattern == resultValue;
@@ -182,9 +246,11 @@
                 }
                 else
                 {
+                    // If the match type is greater or less than, the values must be numeric.
                     if (decimal.TryParse(expectedPattern, out decimal expectedNumeric) &&
                         decimal.TryParse(resultValue, out decimal resultNumeric))
                     {
+                        // Compare the resulting decimals.
                         match = (matchType == MatchType.GreaterThan && resultNumeric > expectedNumeric) ||
                                 (matchType == MatchType.LessThan    && resultNumeric < expectedNumeric);
                     }
@@ -195,8 +261,11 @@
                 }
             }
 
+            // We have determined whether the result value matches the expected pattern.
+            // Generate a check result accordingly.
             if (matchType.In(MatchType.Equals, MatchType.Contains))
             {
+                // Equals, Contains: the strings are supposed to match.
                 if (match)
                     return Pass(string.Format("{0} {1} the pattern: {2}", description, matchType.ToString().ToLower(), expectedPattern));
                 else
@@ -204,6 +273,8 @@
             }
             else if (matchType.In(MatchType.NotEquals, MatchType.NotContains))
             {
+                // NotEquals, NotContains: the strings are not supposed to match.
+                // So, fail if they do match and pass if they do not.
                 if (match)
                     return Fail(string.Format("{0} {1} the pattern: {2} ({0}: {3})", description, matchType.ToString().ToLower().Replace("not", ""), expectedPattern, resultValue));
                 else
@@ -211,6 +282,7 @@
             }
             else
             {
+                // GreaterThan, LessThan
                 if (match)
                     return Pass(string.Format("{0} ({1}) is {2} {3}", description, resultValue, matchType.ToString().ToLower().Replace("than", " than"), expectedPattern));
                 else
@@ -218,6 +290,14 @@
             }
         }
 
+        /// <summary>Merges multiple execution results.</summary>
+        /// <param name="results">The results to merge.</param>
+        /// <returns>A single result containing the messages of all the input results, and a failure status if any of the input results failed.</returns>
+        /// <remarks>
+        /// Some checks may want to run several tests on the result of a remote command.
+        /// After collecting their results, they can use this method to combine them into
+        /// a single result that will be reported to the user.
+        /// </remarks>
         protected CheckResult MergeResults(params CheckResult[] results)
         {
             StringBuilder message = new StringBuilder();
@@ -226,13 +306,18 @@
             {
                 if (result == null)
                     continue;
+                // Report failure if any of the results has failed.
                 if (result.Failed)
                     failed = true;
+                // Merge the result messages.
                 message.AppendLine(result.Message);
             }
             return failed ? Fail(message.ToString().Trim()) : Pass(message.ToString().Trim());
         }
 
+        /// <summary>Executes the check asynchronously.</summary>
+        /// <param name="token">A token for cancelling the execution.</param>
+        /// <returns>The result of the check execution.</returns>
         protected abstract Task<CheckResult> ExecuteCheckAsync(CancellationToken token);
     }
 }
\ No newline at end of file