Mercurial > servermonitor
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