# HG changeset patch # User Brad Greco # Date 1555981842 14400 # Node ID 2db36ab759de9bb46110e9fd0130ab75bbd6de8e # Parent a36cc5c123f45d5f8ce8a12cd1120626e4cc8e90 Add comments. diff -r a36cc5c123f4 -r 2db36ab759de ServerMonitor/Forms/QuickHelpForm.cs --- a/ServerMonitor/Forms/QuickHelpForm.cs Mon Apr 15 19:25:27 2019 -0400 +++ b/ServerMonitor/Forms/QuickHelpForm.cs Mon Apr 22 21:10:42 2019 -0400 @@ -1,15 +1,10 @@ using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data; using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Windows.Forms; namespace ServerMonitorApp { + /// Form for showing help text in a floating popup. public partial class QuickHelpForm : Form { private string Rtf; @@ -19,6 +14,8 @@ InitializeComponent(); } + /// Creates a help popup form with the provided text. + /// Text to show in the popup in RTF format. public QuickHelpForm(string rtf) { InitializeComponent(); @@ -27,29 +24,37 @@ private void QuickHelpForm_Load(object sender, EventArgs e) { - (Owner as CheckForm).HelpLocationChanged += Owner_HelpPositionChanged; - HelpTextBox.ContentsResized += HelpTextBox_ContentsResized; // Causes form to resize after Rtf is assigned + // Subscribe to the owner's notifications that the location of + // the Help button that triggered this popup has changed. + (Owner as CheckForm).HelpLocationChanged += Owner_HelpLocationChanged; + // Subscribe to this event before setting the text box's text + // so we can resize the form to match the text size. + HelpTextBox.ContentsResized += HelpTextBox_ContentsResized; HelpTextBox.Rtf = Rtf; } + /// Resizes the popup form to fit the displayed text. private void HelpTextBox_ContentsResized(object sender, ContentsResizedEventArgs e) { Height = e.NewRectangle.Height + 12; } - private void Owner_HelpPositionChanged(object sender, HelpLocationChangedEventArgs e) + /// Moves the popup form to always appear near the Help button that triggered it. + private void Owner_HelpLocationChanged(object sender, HelpLocationChangedEventArgs e) { Point location = e.HelpLocation; location.Offset(24, 0); Location = location; } + // Hides the popup form when ESC is pressed. private void Control_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode == Keys.Escape) Close(); } + // Hides the popup form when it is clicked. private void Control_Click(object sender, EventArgs e) { Close(); diff -r a36cc5c123f4 -r 2db36ab759de ServerMonitor/Forms/ServerForm.cs --- a/ServerMonitor/Forms/ServerForm.cs Mon Apr 15 19:25:27 2019 -0400 +++ b/ServerMonitor/Forms/ServerForm.cs Mon Apr 22 21:10:42 2019 -0400 @@ -1,19 +1,16 @@ -using Renci.SshNet; -using Renci.SshNet.Common; -using ServerMonitorApp.Properties; +using ServerMonitorApp.Properties; using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; -using System.IO; using System.Linq; -using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace ServerMonitorApp { + /// Form for adding or editing a server and managing its checks. public partial class ServerForm : Form { private bool isNewServer; @@ -25,18 +22,26 @@ private readonly Dictionary checkForms = new Dictionary(); private readonly Dictionary filterChecks; + /// The server being edited. public Server Server { get; private set; } + /// The checks currently selected by the user. private IEnumerable SelectedChecks => CheckGrid.SelectedRows.Cast().Select(r => r.DataBoundItem).Cast(); + /// The first check currently selected by the user. private Check SelectedCheck => SelectedChecks.FirstOrDefault(); + /// ServerForm constructor. + /// The global server monitor object. + /// The server to edit. + /// Whether an existing server is being edited or a new server is being created. public ServerForm(ServerMonitor monitor, Server server, bool isNewServer = false) { InitializeComponent(); this.monitor = monitor; this.isNewServer = isNewServer; Server = server; + // Associates filter check boxes with their corresponding check statuses. filterChecks = new Dictionary { { LogSuccessCheckBox, CheckStatus.Success }, @@ -48,16 +53,20 @@ private void ServerForm_Load(object sender, EventArgs e) { + // Bind the Check grid to the server's checks. CheckBindingSource.DataSource = Server.Checks; monitor.CheckStatusChanged += Monitor_CheckStatusChanged; + // Deselect the default first row selection. CheckGrid.ClearSelection(); if (isNewServer) { + // Set defaults for a new server. LoginComboBox.SelectedIndex = 0; Icon = CheckStatus.Success.GetIcon(); } else { + // Update inputs with the server information. SetTitle(); SetIcon(); NameTextBox.Text = Server.Name; @@ -71,14 +80,20 @@ changedPassword = false; } + // After the input controls have been initialized, bind change listeners to them. BindChangeListeners(); + // Resize the images in buttons to fit the button size. FormatImageButtons(); + // Set the Run and Edit buttons to their default state. UpdateCheckButtons(); + // Focus the name text box if it is empty so the user can start typing immediately. if (NameTextBox.Text == string.Empty) ActiveControl = NameTextBox; } + /// Shows the form. + /// Whether the form should be activated. Otherwise, it will be shown minimized. public void Show(bool activate) { if (!activate) @@ -86,21 +101,26 @@ Show(); } + /// Updates the form when the status of a check changes. private void Monitor_CheckStatusChanged(object sender, CheckStatusChangedEventArgs e) { + // Ignore events for checks that belong to other servers. if (e.Check.Server != Server) return; + // Refresh the check display with the updated values. CheckGrid.Refresh(); SetIcon(); + // If there is a result, and the log grid has been initialized, append the log entry. if (e.CheckResult != null && logResults != null) { logResults.Insert(0, e.CheckResult); + // If a filter is applied, also append the log to the filtered list if it matches. if (logResultsFiltered != null && MatchesFilter(e.CheckResult)) logResultsFiltered.Insert(0, e.CheckResult); } } - /// Update the server with the current input values + /// Updates the server with the current input values. /// /// If true, immediately update the config file on disk. /// If false, only update the config file if it has not been recently updated. @@ -117,6 +137,8 @@ if (changedPassword) Server.Password = PasswordTextBox.Text; + // If a force save is not requested, only save if the last save time is + // old enough so we don't end up writing out the file on every keystroke. if (forceSave || lastSaveTime < DateTime.Now.AddSeconds(-5)) { lastSaveTime = DateTime.Now; @@ -124,6 +146,7 @@ } } + /// Sets the window title and header based on the server name and state. private void SetTitle(string title = null) { title = (title ?? Server.Name) + (Server.Enabled ? "" : " (disabled)"); @@ -131,57 +154,70 @@ TitleLabel.Text = title; } + /// Sets the window icon based on the status of the server. private void SetIcon() { if (Server != null) Icon = Server.Status.GetIcon(); } + /// Updates the window title when the server name changes. private void NameTextBox_TextChanged(object sender, EventArgs e) { SetTitle(NameTextBox.Text); } + /// Shows the password or private key controls when the login type changes. private void LoginComboBox_SelectedIndexChanged(object sender, EventArgs e) { if (LoginComboBox.SelectedIndex == (int)LoginType.PrivateKey) { + // Show private key controls. PasswordTextBox.Visible = false; KeyTextBox.Visible = true; KeyBrowseButton.Visible = true; } else { + // Show password controls. PasswordTextBox.Visible = true; KeyTextBox.Visible = false; KeyBrowseButton.Visible = false; } } + /// Shows a form to create a new check. private void NewCheckButton_Click(object sender, EventArgs e) { ShowCheckForm(null); } + /// Shows a form to edit the selected check. private void EditCheckButton_Click(object sender, EventArgs e) { EditSelectedCheck(); } + /// Executes the selected checks. private void RunButton_Click(object sender, EventArgs e) { ExecuteChecks(SelectedChecks); } + /// Executes all checks for the server. private void RunAllButton_Click(object sender, EventArgs e) { ExecuteChecks(Server.Checks); } + /// Shows a form to create or edit a check. + /// The check to edit, or null to create a new check. private void ShowCheckForm(Check check) { if (check != null) { + // Activate the form to edit the check if it is already open. + // Otherwise, open a new form. if (!checkForms.TryGetValue(check.Id, out CheckForm form)) { form = new CheckForm(monitor, check); @@ -200,43 +236,55 @@ } } + /// Shows a form to edit the selected check. private void EditSelectedCheck() { ShowCheckForm(SelectedCheck); } + /// Deletes the selected checks. private void DeleteSelectedChecks() { + // Prompt to delete unless the "Do not ask again" setting has been set. if (Settings.Default.ConfirmDeleteCheck) { string message = "Delete " + (SelectedChecks.Count() == 1 ? "\"" + SelectedCheck.Name + "\"" : "selected checks") + "?"; using (CheckBoxDialog dialog = new CheckBoxDialog { Message = message }) { DialogResult result = dialog.ShowDialog(); + // Save the "Do not ask again" setting only if OK was clicked. if (dialog.Checked && result == DialogResult.OK) { Settings.Default.ConfirmDeleteCheck = false; Settings.Default.Save(); } + // Do nothing if Cancel was clicked. if (result != DialogResult.OK) return; } } + // If OK was clicked or no confirmation was shown, delete the checks. foreach (Check check in SelectedChecks) Server.DeleteCheck(check); } + /// Executes the selected checks. private async void ExecuteChecks(IEnumerable checks) { await Task.WhenAll(checks.Select(c => monitor.ExecuteCheckAsync(c))); } + /// Shows the execution history for a check. + /// The check to show execution history for. public void ShowLog(Check check) { + // Switch to the Log tab. CheckTabControl.SelectedTab = LogTabPage; + // Filter the list to only show history for this check. LogCheckComboBox.SelectedItem = check; } + /// Sets the enabled state of buttons based on whether checks are selected. private void UpdateCheckButtons() { RunAllButton.Enabled = CheckGrid.RowCount > 0; @@ -244,6 +292,7 @@ EditCheckButton.Enabled = CheckGrid.SelectedRows.Count == 1; } + /// Resizes the images in buttons to fit the button size. private void FormatImageButtons() { Button[] buttons = new Button[] { NewCheckButton, RunAllButton, RunButton, EditCheckButton, DeleteCheckButton }; @@ -251,46 +300,59 @@ Helpers.FormatImageButton(button); } + /// Binds change listeners to most input controls. private void BindChangeListeners() { Server.EnabledChanged += Server_EnabledChanged; + // Update the server with text box values. foreach (Control control in ServerInfoPanel.Controls.OfType().Where(c => c is TextBox)) control.TextChanged += (sender, e) => UpdateServer(false); + // Update the server with combo box values. foreach (Control control in ServerInfoPanel.Controls.OfType().Where(c => c is ComboBox)) control.TextChanged += (sender, e) => UpdateServer(); + // Apply the log filter when the filter checkboxes are changed. foreach (CheckBox control in LogTabPage.Controls.OfType()) control.CheckedChanged += FilterChanged; } + /// Handles the closing of a check form. private void CheckForm_FormClosing(object sender, FormClosingEventArgs e) { + // Remove the form from the list of open forms. CheckForm form = (CheckForm)sender; form.FormClosing -= CheckForm_FormClosing; checkForms.Remove(form.CheckId); + // Refresh the check grid if the check was modified. if (form.DialogResult == DialogResult.OK) CheckGrid.Refresh(); } + /// Updates the state of buttons based on the check selection. private void CheckGrid_SelectionChanged(object sender, EventArgs e) { UpdateCheckButtons(); } + /// Sets a flag indicating the password text box contains a real password that should be saved. + /// When the form is loaded, the password text box is populated with literal asterisks, not the saved password. private void PasswordTextBox_TextChanged(object sender, EventArgs e) { changedPassword = true; } + /// Saves the server when the form is closed. private void ServerForm_FormClosing(object sender, FormClosingEventArgs e) { UpdateServer(); } + /// Edits the selected check when it is double clicked. private void CheckGrid_CellDoubleClick(object sender, DataGridViewCellEventArgs e) { EditSelectedCheck(); } + /// Edits the selected check when ENTER is pressed. private void CheckGrid_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode == Keys.Enter) @@ -300,12 +362,14 @@ } } + /// Deletes the selected checks. private void DeleteCheckButton_Click(object sender, EventArgs e) { DeleteSelectedChecks(); UpdateServer(); } + /// Shows an icon next to each check indicating the last execution status. private void CheckGrid_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e) { if (e.ColumnIndex == StatusColumn.Index) @@ -314,6 +378,7 @@ } } + /// Shows an icon next to each log entry indicating its execution status. private void LogGrid_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e) { if (e.ColumnIndex == LogStatusColumn.Index) @@ -322,6 +387,7 @@ } } + /// Refreshes the check filter combo box when the list of checks changes. private void CheckBindingSource_ListChanged(object sender, ListChangedEventArgs e) { if (Server?.Checks != null) @@ -333,8 +399,10 @@ } } + /// Handles showing the check execution log when the Log tab is selected. private void CheckTabControl_SelectedIndexChanged(object sender, EventArgs e) { + // The results grid is not always used, and so is initialized just in time. if (logResults == null && CheckTabControl.SelectedTab == LogTabPage) { logResults = new BindingList(monitor.GetLog(Server)); @@ -342,24 +410,28 @@ } } + /// Shows a hand cursor over the status column as a hint that it can be clicked to jump to the log. private void CheckGrid_CellMouseEnter(object sender, DataGridViewCellEventArgs e) { if (e.ColumnIndex == StatusColumn.Index) CheckGrid.Cursor = Cursors.Hand; } + /// Restores the cursor to its default when leaving the status column. private void CheckGrid_CellMouseLeave(object sender, DataGridViewCellEventArgs e) { if (e.ColumnIndex == StatusColumn.Index) CheckGrid.Cursor = Cursors.Default; } + /// Jumps to the check log when the status icon is clicked. private void CheckGrid_CellClick(object sender, DataGridViewCellEventArgs e) { if (e.ColumnIndex == StatusColumn.Index) ShowLog((Check)CheckBindingSource[e.RowIndex]); } + /// Enables or disables a check when the enable column is clicked. private void CheckGrid_CellContentClick(object sender, DataGridViewCellEventArgs e) { // The status column is set to read-only, and updates are manually done here. @@ -367,30 +439,37 @@ if (e.ColumnIndex == EnabledColumn.Index) { Check check = (Check)CheckBindingSource[e.RowIndex]; - check.Enabled = !(bool)CheckGrid.Rows[e.RowIndex].Cells[e.ColumnIndex].Value; ; + check.Enabled = !(bool)CheckGrid.Rows[e.RowIndex].Cells[e.ColumnIndex].Value; Server.UpdateCheck(check); } } + /// Refreshes the log when a log filter control is changed. private void FilterChanged(object sender, EventArgs e) { + // Determine which check statuses to show. filteredStatuses = filterChecks.Where(fc => fc.Key.Checked).Select(fc => fc.Value).ToArray(); if (filteredStatuses.Length == filterChecks.Count && LogCheckComboBox.SelectedIndex == 0) { + // If all statuses are shown and no check is selected, show all log entries. LogGrid.DataSource = logResults; + // Unset the filtered list so it can be garbage collected. logResultsFiltered = null; } else { + // If any filter is applied, create and display a new list with the filtered log entries. logResultsFiltered = new BindingList(logResults.Where(result => MatchesFilter(result)).ToList()); LogGrid.DataSource = logResultsFiltered; } } + /// Stops the taskbar button flashing when the window receives focus. private void ServerForm_Activated(object sender, EventArgs e) { Win32Helpers.StopFlashWindowEx(this); } + /// Opens a file browser to select a private key file. private void KeyBrowseButton_Click(object sender, EventArgs e) { OpenFileDialog dialog = new OpenFileDialog() { Title = "Select private key file" }; @@ -401,32 +480,47 @@ } } + /// Tests whether a log entry matches the active filter. + /// The log entry to test agains the active filter. private bool MatchesFilter(CheckResult result) { return filteredStatuses.Contains(result.CheckStatus) && (LogCheckComboBox.SelectedIndex == 0 || LogCheckComboBox.SelectedItem == result.Check); } + /// Attempts to open the private key when the private key textbox loses focus. private void KeyTextBox_Leave(object sender, EventArgs e) { OpenPrivateKey(monitor, Server, this); } + /// Attempts to open the private key, collecting a password if necessary, and displays a message if it cannot be opened. + /// The global server monitor object. + /// The server associated with the keyfile to open. + /// The window to use as the owner for password and message boxes. public static void OpenPrivateKey(ServerMonitor monitor, Server server, IWin32Window owner) { + // Nothing to do if the server does not use a private key or one has not been set up yet. if (server.LoginType != LoginType.PrivateKey || server.KeyFile.IsNullOrEmpty()) return; + // Attempt to open the keyfile. KeyStatus keyStatus = monitor.OpenPrivateKey(server.KeyFile); + // If the key is encrypted and has not been opened yet, ask for the password. if (keyStatus == KeyStatus.NeedPassword) { string message = "Private key password for " + server.Name + ":"; Icon icon = SystemIcons.Question; + // Attempt to open the keyfile until the correct password is entered or Cancel is clicked. while (keyStatus != KeyStatus.Open) { + // Collect the password. string password = InputDialog.ShowDialog(message, icon, owner); + // Stop asking if Cancel was clicked. if (password == null) return; + // Try to open the key using the collected password. keyStatus = monitor.OpenPrivateKey(server.KeyFile, password); + // If the password was incorrect, try again with a message saying so. if (keyStatus == KeyStatus.NeedPassword) { message = "Incorrect private key password for " + server.Name + ", please try again:"; @@ -436,31 +530,28 @@ } else if (keyStatus == KeyStatus.NotAccessible) { + // If the private key is not accessible, there is nothing we can do but let the user know. MessageBox.Show("The private key file " + server.KeyFile + " is not accessible or does not exist.", "Cannot open private key", MessageBoxButtons.OK, MessageBoxIcon.Error); - return; } } + /// Enables or disables the server when the Enabled box is clicked. private void EnabledCheckBox_Click(object sender, EventArgs e) { bool enabled = EnabledCheckBox.Checked; + // The private key may not be open yet when enabling a server, so do that now. if (enabled) OpenPrivateKey(monitor, Server, this); Server.Enabled = enabled; EnabledCheckBox.Checked = Server.Enabled; } - //private void EnabledCheckBox_CheckedChanged(object sender, EventArgs e) - //{ - // bool enabled = EnabledCheckBox.Checked; - // if (enabled) - // OpenPrivateKey(monitor, Server, this); - // EnabledCheckBox.Checked = Server.Enabled; - //} - + /// Updates the title and enabled check box when the server is enabled or disabled. private void Server_EnabledChanged(object sender, EventArgs e) { SetTitle(); + // The server can also be enabled or disabled from the main server + // summary form, so update the checkbox when that happens. EnabledCheckBox.Checked = Server.Enabled; } }