view ServerMonitor/Forms/ServerForm.cs @ 18:b713b9db4c82

HTTP checks.
author Brad Greco <brad@bgreco.net>
date Mon, 27 May 2019 15:40:44 -0400
parents 2db36ab759de
children 7645122aa7a9
line wrap: on
line source

using ServerMonitorApp.Properties;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace ServerMonitorApp
{
    /// <summary>Form for adding or editing a server and managing its checks.</summary>
    public partial class ServerForm : Form
    {
        private bool isNewServer;
        private bool changedPassword;
        private DateTime lastSaveTime;
        private ServerMonitor monitor;
        private BindingList<CheckResult> logResults, logResultsFiltered;
        private CheckStatus[] filteredStatuses;
        private readonly Dictionary<int, CheckForm> checkForms = new Dictionary<int, CheckForm>();
        private readonly Dictionary<CheckBox, CheckStatus> filterChecks;

        /// <summary>The server being edited.</summary>
        public Server Server { get; private set; }

        /// <summary>The checks currently selected by the user.</summary>
        private IEnumerable<Check> SelectedChecks => CheckGrid.SelectedRows.Cast<DataGridViewRow>().Select(r => r.DataBoundItem).Cast<Check>();

        /// <summary>The first check currently selected by the user.</summary>
        private Check SelectedCheck => SelectedChecks.FirstOrDefault();

        /// <summary>ServerForm constructor.</summary>
        /// <param name="monitor">The global server monitor object.</param>
        /// <param name="server">The server to edit.</param>
        /// <param name="isNewServer">Whether an existing server is being edited or a new server is being created.</param>
        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<CheckBox, CheckStatus>
            {
                { LogSuccessCheckBox, CheckStatus.Success },
                { LogInformationCheckBox, CheckStatus.Information },
                { LogWarningCheckBox, CheckStatus.Warning },
                { LogErrorCheckBox, CheckStatus.Error },
            };
        }

        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;
                HostTextBox.Text = Server.Host;
                EnabledCheckBox.Checked = Server.Enabled;
                PortTextBox.Text = Server.Port.ToString();
                UsernameTextBox.Text = Server.Username;
                PasswordTextBox.Text = "********************";
                LoginComboBox.SelectedIndex = (int)Server.LoginType;
                KeyTextBox.Text = Server.KeyFile;
                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;
        }

        /// <summary>Shows the form.</summary>
        /// <param name="activate">Whether the form should be activated. Otherwise, it will be shown minimized.</param>
        public void Show(bool activate)
        {
            if (!activate)
                WindowState = FormWindowState.Minimized;
            Show();
        }

        /// <summary>Updates the form when the status of a check changes.</summary>
        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);
            }
        }

        /// <summary>Updates the server with the current input values.</summary>
        /// <param name="forceSave">
        /// If true, immediately update the config file on disk.
        /// If false, only update the config file if it has not been recently updated.
        /// </param>
        private void UpdateServer(bool forceSave = true)
        {
            Server.Name = NameTextBox.Text;
            Server.Host = HostTextBox.Text.Trim();
            Server.Enabled = EnabledCheckBox.Checked;
            Server.Port = int.TryParse(PortTextBox.Text, out int port) ? port : 0;
            Server.Username = UsernameTextBox.Text.Trim();
            Server.LoginType = (LoginType)LoginComboBox.SelectedIndex;
            Server.KeyFile = KeyTextBox.Text.Trim();
            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;
                monitor.SaveServers();
            }
        }

        /// <summary>Sets the window title and header based on the server name and state.</summary>
        private void SetTitle(string title = null)
        {
            title = (title ?? Server.Name) + (Server.Enabled ? "" : " (disabled)");
            Text = title;
            TitleLabel.Text = title;
        }

        /// <summary>Sets the window icon based on the status of the server.</summary>
        private void SetIcon()
        {
            if (Server != null)
                Icon = Server.Status.GetIcon();
        }

        /// <summary>Updates the window title when the server name changes.</summary>
        private void NameTextBox_TextChanged(object sender, EventArgs e)
        {
            SetTitle(NameTextBox.Text);
        }

        /// <summary>Shows the password or private key controls when the login type changes.</summary>
        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;
            }
        }

        /// <summary>Shows a form to create a new check.</summary>
        private void NewCheckButton_Click(object sender, EventArgs e)
        {
            ShowCheckForm(null);
        }

        /// <summary>Shows a form to edit the selected check.</summary>
        private void EditCheckButton_Click(object sender, EventArgs e)
        {
            EditSelectedCheck();
        }

        /// <summary>Executes the selected checks.</summary>
        private void RunButton_Click(object sender, EventArgs e)
        {
            ExecuteChecks(SelectedChecks);
        }

        /// <summary>Executes all checks for the server.</summary>
        private void RunAllButton_Click(object sender, EventArgs e)
        {
            ExecuteChecks(Server.Checks);
        }

        /// <summary>Shows a form to create or edit a check.</summary>
        /// <param name="check">The check to edit, or null to create a new check.</param>
        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);
                    checkForms[check.Id] = form;
                    form.FormClosing += CheckForm_FormClosing;
                    form.Show();
                }
                else
                {
                    form.Activate();
                }
            }
            else
            {
                new CheckForm(monitor, Server).Show();
            }
        }

        /// <summary>Shows a form to edit the selected check.</summary>
        private void EditSelectedCheck()
        {
            ShowCheckForm(SelectedCheck);
        }

        /// <summary>Deletes the selected checks.</summary>
        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);
        }

        /// <summary>Executes the selected checks.</summary>
        private async void ExecuteChecks(IEnumerable<Check> checks)
        {
            await Task.WhenAll(checks.Select(c => monitor.ExecuteCheckAsync(c)));
        }

        /// <summary>Shows the execution history for a check.</summary>
        /// <param name="check">The check to show execution history for.</param>
        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;
        }

        /// <summary>Sets the enabled state of buttons based on whether checks are selected.</summary>
        private void UpdateCheckButtons()
        {
            RunAllButton.Enabled = CheckGrid.RowCount > 0;
            RunButton.Enabled = DeleteCheckButton.Enabled = CheckGrid.SelectedRows.Count > 0;
            EditCheckButton.Enabled = CheckGrid.SelectedRows.Count == 1;
        }

        /// <summary>Resizes the images in buttons to fit the button size.</summary>
        private void FormatImageButtons()
        {
            Button[] buttons = new Button[] { NewCheckButton, RunAllButton, RunButton, EditCheckButton, DeleteCheckButton };
            foreach (Button button in buttons)
                Helpers.FormatImageButton(button);
        }

        /// <summary>Binds change listeners to most input controls.</summary>
        private void BindChangeListeners()
        {
            Server.EnabledChanged += Server_EnabledChanged;
            // Update the server with text box values.
            foreach (Control control in ServerInfoPanel.Controls.OfType<Control>().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<Control>().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<CheckBox>())
                control.CheckedChanged += FilterChanged;
        }

        /// <summary>Handles the closing of a check form.</summary>
        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();
        }

        /// <summary>Updates the state of buttons based on the check selection.</summary>
        private void CheckGrid_SelectionChanged(object sender, EventArgs e)
        {
            UpdateCheckButtons();
        }

        /// <summary>Sets a flag indicating the password text box contains a real password that should be saved.</summary>
        /// <remarks>When the form is loaded, the password text box is populated with literal asterisks, not the saved password.</remarks>
        private void PasswordTextBox_TextChanged(object sender, EventArgs e)
        {
            changedPassword = true;
        }

        /// <summary>Saves the server when the form is closed.</summary>
        private void ServerForm_FormClosing(object sender, FormClosingEventArgs e)
        {
            UpdateServer();
        }

        /// <summary>Edits the selected check when it is double clicked.</summary>
        private void CheckGrid_CellDoubleClick(object sender, DataGridViewCellEventArgs e)
        {
            EditSelectedCheck();
        }

        /// <summary>Edits the selected check when ENTER is pressed.</summary>
        private void CheckGrid_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.KeyCode == Keys.Enter)
            {
                EditSelectedCheck();
                e.Handled = true;
            }
        }

        /// <summary>Deletes the selected checks.</summary>
        private void DeleteCheckButton_Click(object sender, EventArgs e)
        {
            DeleteSelectedChecks();
            UpdateServer();
        }

        /// <summary>Shows an icon next to each check indicating the last execution status.</summary>
        private void CheckGrid_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e)
        {
            if (e.ColumnIndex == StatusColumn.Index)
            {
                e.Value = ((CheckStatus)e.Value).GetImage();
            }
        }

        /// <summary>Shows an icon next to each log entry indicating its execution status.</summary>
        private void LogGrid_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e)
        {
            if (e.ColumnIndex == LogStatusColumn.Index)
            {
                e.Value = ((CheckStatus)e.Value).GetImage();
            }
        }

        /// <summary>Refreshes the check filter combo box when the list of checks changes.</summary>
        private void CheckBindingSource_ListChanged(object sender, ListChangedEventArgs e)
        {
            if (Server?.Checks != null)
            {
                LogCheckComboBox.Items.Clear();
                LogCheckComboBox.Items.Add("(All)");
                LogCheckComboBox.Items.AddRange(Server.Checks.ToArray());
                LogCheckComboBox.SelectedIndex = 0;
            }
        }

        /// <summary>Handles showing the check execution log when the Log tab is selected.</summary>
        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<CheckResult>(monitor.GetLog(Server));
                LogGrid.DataSource = logResults;
            }
        }

        /// <summary>Shows a hand cursor over the status column as a hint that it can be clicked to jump to the log.</summary>
        private void CheckGrid_CellMouseEnter(object sender, DataGridViewCellEventArgs e)
        {
            if (e.ColumnIndex == StatusColumn.Index)
                CheckGrid.Cursor = Cursors.Hand;
        }

        /// <summary>Restores the cursor to its default when leaving the status column.</summary>
        private void CheckGrid_CellMouseLeave(object sender, DataGridViewCellEventArgs e)
        {
            if (e.ColumnIndex == StatusColumn.Index)
                CheckGrid.Cursor = Cursors.Default;
        }

        /// <summary>Jumps to the check log when the status icon is clicked.</summary>
        private void CheckGrid_CellClick(object sender, DataGridViewCellEventArgs e)
        {
            if (e.ColumnIndex == StatusColumn.Index)
                ShowLog((Check)CheckBindingSource[e.RowIndex]);
        }

        /// <summary>Enables or disables a check when the enable column is clicked.</summary>
        private void CheckGrid_CellContentClick(object sender, DataGridViewCellEventArgs e)
        {
            // The status column is set to read-only, and updates are manually done here.
            // Otherwise, the change doesn't take effect until the cell loses focus.
            if (e.ColumnIndex == EnabledColumn.Index)
            {
                Check check = (Check)CheckBindingSource[e.RowIndex];
                check.Enabled = !(bool)CheckGrid.Rows[e.RowIndex].Cells[e.ColumnIndex].Value;
                Server.UpdateCheck(check);
            }
        }

        /// <summary>Refreshes the log when a log filter control is changed.</summary>
        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<CheckResult>(logResults.Where(result => MatchesFilter(result)).ToList());
                LogGrid.DataSource = logResultsFiltered;
            }
        }

        /// <summary>Stops the taskbar button flashing when the window receives focus.</summary>
        private void ServerForm_Activated(object sender, EventArgs e)
        {
            Win32Helpers.StopFlashWindowEx(this);
        }

        /// <summary>Opens a file browser to select a private key file.</summary>
        private void KeyBrowseButton_Click(object sender, EventArgs e)
        {
            OpenFileDialog dialog = new OpenFileDialog() { Title = "Select private key file" };
            if (dialog.ShowDialog() == DialogResult.OK)
            {
                KeyTextBox.Text = dialog.FileName;
                UpdateServer();
            }
        }

        /// <summary>Tests whether a log entry matches the active filter.</summary>
        /// <param name="result">The log entry to test agains the active filter.</param>
        private bool MatchesFilter(CheckResult result)
        {
            return filteredStatuses.Contains(result.CheckStatus) && (LogCheckComboBox.SelectedIndex == 0 || LogCheckComboBox.SelectedItem == result.Check);
        }

        /// <summary>Attempts to open the private key when the private key textbox loses focus.</summary>
        private void KeyTextBox_Leave(object sender, EventArgs e)
        {
            OpenPrivateKey(monitor, Server, this);
        }

        /// <summary>Attempts to open the private key, collecting a password if necessary, and displays a message if it cannot be opened.</summary>
        /// <param name="monitor">The global server monitor object.</param>
        /// <param name="server">The server associated with the keyfile to open.</param>
        /// <param name="owner">The window to use as the owner for password and message boxes.</param>
        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:";
                        icon = SystemIcons.Error;
                    }
                }
            }
            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);
            }
        }

        /// <summary>Enables or disables the server when the Enabled box is clicked.</summary>
        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;
        }

        /// <summary>Updates the title and enabled check box when the server is enabled or disabled.</summary>
        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;
        }
    }
}