view ServerMonitor/Forms/ServerSummaryForm.cs @ 34:9c0e18d65e8b

Build NuGet from source instead of using the NuGet package to fix the update notification always showing when the program is run from Windows startup.
author Brad Greco <brad@bgreco.net>
date Sat, 13 Jul 2019 12:09:10 -0400
parents b0af6b4bed4d
children
line wrap: on
line source

´╗┐using NAppUpdate.Framework;
using NAppUpdate.Framework.Sources;
using ServerMonitorApp.Properties;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
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>();
        private ServerMonitor monitor;

        public ServerSummaryForm()
        {
            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(AboutButton);
            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)
        {
            // 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 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);
            NotifyIcon.Tag = result;
            NotifyIcon.ShowBalloonTip(30000, title, result.Message, GetToolTipIcon(result.CheckStatus));
        }

        /// <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
                    : s.KeyStatus == KeyStatus.NeedPassword ? CheckStatus.Warning : CheckStatus.Success)
                .DefaultIfEmpty(CheckStatus.Success)
                .Max();
            Icon = status.GetIcon();
            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)
            {
                RefreshServer(e.Check.Server);
            }
        }

        /// <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)
            {
                case CheckStatus.Error: return ToolTipIcon.Error;
                case CheckStatus.Warning: return ToolTipIcon.Warning;
                case CheckStatus.Information: return ToolTipIcon.Info;
                default: return ToolTipIcon.None;
            }
        }

        /// <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);
            }
            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)
            {
                Hide();
                e.Cancel = true;
            }
        }

        /// <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;
            ServerForm form = ShowServerForm(result.Check.Server);
            form.ShowLog(result.Check);
            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);
                if (result == DialogResult.OK)
                {
                    monitor.DeleteServer(server);
                    RefreshDisplay();
                }
            }
            else if (e.ClickedItem == ToggleEnableServerMenuItem)
            {
                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;
                RefreshDisplay();
            }
        }

        /// <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)
                ShowWindow();
            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)
                ShowWindow();
            else if (e.Button == MouseButtons.Right)
                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)
                ShowWindow();
            else if (e.ClickedItem == ExitMenuItem)
                Application.Exit();
        }

        /// <summary>Begins checking for program updates in the background.</summary>
        private void CheckForUpdate()
        {
            // The latest NuGet package of NAppUpdate (0.5.1) always thinks that an update is available
            // when the program is run from Windows startup (https://github.com/synhershko/NAppUpdate/pull/122).
            // The fix has already been made but not released, so the version of NAppUpdate included in
            // this project was built from source instead of using the NuGet package.
            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/servermonitor/update.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;
            manager.EndCheckForUpdates(result);
            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
                + "What's new:" + Environment.NewLine
                + "{1}" + Environment.NewLine
                + Environment.NewLine
                + "Would you like to download and apply the update now?";
            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];
        }

        private void AboutButton_Click(object sender, EventArgs e)
        {
            new AboutForm().Show();
        }
    }
}