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
{
/// Form for adding or editing a server and managing its checks.
public partial class ServerForm : Form
{
private bool isNewServer;
private bool changedPassword;
private DateTime lastSaveTime;
private ServerMonitor monitor;
private BindingList logResults, logResultsFiltered;
private CheckStatus[] filteredStatuses;
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 },
{ 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;
}
/// Shows the form.
/// Whether the form should be activated. Otherwise, it will be shown minimized.
public void Show(bool activate)
{
if (!activate)
WindowState = FormWindowState.Minimized;
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);
}
}
/// 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.
///
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();
}
}
/// 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)");
Text = title;
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);
checkForms[check.Id] = form;
form.FormClosing += CheckForm_FormClosing;
form.Show();
}
else
{
form.Activate();
}
}
else
{
new CheckForm(monitor, Server).Show();
}
}
/// 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;
RunButton.Enabled = DeleteCheckButton.Enabled = CheckGrid.SelectedRows.Count > 0;
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 };
foreach (Button button in buttons)
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)
{
EditSelectedCheck();
e.Handled = true;
}
}
/// 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)
{
e.Value = ((CheckStatus)e.Value).GetImage();
}
}
/// 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)
{
e.Value = ((CheckStatus)e.Value).GetImage();
}
}
/// Refreshes the check filter combo box when the list of checks changes.
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;
}
}
/// 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));
LogGrid.DataSource = logResults;
}
}
/// 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.
// 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);
}
}
/// 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" };
if (dialog.ShowDialog() == DialogResult.OK)
{
KeyTextBox.Text = dialog.FileName;
UpdateServer();
}
}
/// 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:";
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);
}
}
/// 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;
}
/// 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;
}
}
}