Mercurial > servermonitor
view ServerMonitor/Objects/ServerMonitor.cs @ 14:2db36ab759de
Add comments.
author | Brad Greco <brad@bgreco.net> |
---|---|
date | Mon, 22 Apr 2019 21:10:42 -0400 |
parents | 052aa62cb42a |
children | 68d7834dc28e |
line wrap: on
line source
using Microsoft.Win32; using Renci.SshNet; using Renci.SshNet.Common; using ServerMonitorApp.Properties; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Net.NetworkInformation; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using System.Xml.Serialization; namespace ServerMonitorApp { public class ServerMonitor { private readonly string configFileDir; private readonly Logger logger; private readonly Dictionary<int, CancellationTokenSource> tokens = new Dictionary<int, CancellationTokenSource>(); private readonly Dictionary<string, PrivateKeyFile> privateKeys = new Dictionary<string, PrivateKeyFile>(); private readonly List<int> pausedChecks = new List<int>(); private bool running, networkAvailable, suspend; private Dictionary<Task<CheckResult>, int> tasks; private ServerSummaryForm mainForm; //private List<Task<CheckResult>> tasks; public event EventHandler<CheckStatusChangedEventArgs> CheckStatusChanged; public List<Server> Servers { get; private set; } = new List<Server>(); public IEnumerable<Check> Checks => Servers.SelectMany(s => s.Checks); public string ConfigFile { get; private set; } public IEnumerable<string> LockedKeys { get { return privateKeys.Where(kvp => kvp.Value == null).Select(kvp => kvp.Key); } } public ServerMonitor(ServerSummaryForm mainForm) { this.mainForm = mainForm; configFileDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ServerMonitor"); ConfigFile = Path.Combine(configFileDir, "servers.xml"); logger = new Logger(Path.Combine(configFileDir, "monitor.log")); } public void AddServer(Server server) { Servers.Add(server); SaveServers(); } public void DeleteServer(Server server) { Servers.Remove(server); SaveServers(); } public void LoadServers() { Read: TextReader reader = null; try { reader = new StreamReader(ConfigFile); XmlSerializer serializer = CreateXmlSerializer(); Servers.Clear(); Servers.AddRange((List<Server>)serializer.Deserialize(reader)); // Update the Checks so they know what Server they belong to. // Would rather do this in the Server object on deserialization, but // that doesn't work when using the XML serializer for some reason. foreach (Server server in Servers) { if (server.LoginType == LoginType.PrivateKey && server.PrivateKeyFile == null) OpenPrivateKey(server.KeyFile); foreach (Check check in server.Checks) { check.Server = server; if (check.Status == CheckStatus.Running) check.Status = check.LastRunStatus; } server.CheckModified += Server_CheckModified; server.EnabledChanged += Server_EnabledChanged; } } // If the file doesn't exist, no special handling is needed. It will be created later. catch (FileNotFoundException) { } catch (DirectoryNotFoundException) { } catch (InvalidOperationException) { reader?.Close(); File.Copy(ConfigFile, ConfigFile + ".error", true); File.Copy(ConfigFile + ".bak", ConfigFile, true); goto Read; } finally { reader?.Close(); } Application.ApplicationExit += Application_ApplicationExit; NetworkChange.NetworkAddressChanged += NetworkChange_NetworkAddressChanged; SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged; logger.TrimLog(); Run(); } public void SaveServers() { GenerateIds(); TextWriter writer = null; XmlSerializer serializer = null; try { File.Copy(ConfigFile, ConfigFile + ".bak", true); } catch { } try { writer = new StreamWriter(ConfigFile); serializer = CreateXmlSerializer(); serializer.Serialize(writer, Servers); } catch (DirectoryNotFoundException) { Directory.CreateDirectory(configFileDir); writer = new StreamWriter(ConfigFile); serializer = CreateXmlSerializer(); serializer.Serialize(writer, Servers); } finally { writer?.Close(); } } private async void Run() { if (running || suspend) return; running = true; networkAvailable = Helpers.IsNetworkAvailable(); if (networkAvailable) { foreach (int id in pausedChecks) { await ExecuteCheckAsync(Checks.FirstOrDefault(c => c.Id == id)); } pausedChecks.Clear(); } tasks = Checks.ToDictionary(c => ScheduleExecuteCheckAsync(c), c => c.Id); while (tasks.Count > 0) { Task<CheckResult> task = await Task.WhenAny(tasks.Keys); tasks.Remove(task); try { CheckResult result = await task; // Result will be null if a scheduled check was disabled if (result != null && result.CheckStatus != CheckStatus.Disabled) tasks.Add(ScheduleExecuteCheckAsync(result.Check), result.Check.Id); } catch (OperationCanceledException) { } } running = false; } public async Task<CheckResult> ExecuteCheckAsync(Check check, CancellationToken token = default(CancellationToken)) { check.Status = CheckStatus.Running; OnCheckStatusChanged(check); CheckResult result = await check.ExecuteAsync(token); if (result.Failed) check.ConsecutiveFailures++; OnCheckStatusChanged(check, result); HandleResultAsync(result); return result; } private void HandleResultAsync(CheckResult result) { logger.Log(result); if (result.Check.ConsecutiveFailures >= result.Check.MaxConsecutiveFailures) { if (result.FailAction == FailAction.FlashTaskbar) mainForm.AlertServerForm(result.Check); if (result.FailAction.In(FailAction.FlashTaskbar, FailAction.NotificationBalloon)) mainForm.ShowBalloon(result); } } public IList<CheckResult> GetLog(Server server) { return logger.Read(server); } private void OnCheckStatusChanged(Check check, CheckResult result = null) { SaveServers(); CheckStatusChanged?.Invoke(check, new CheckStatusChangedEventArgs(check, result)); } private void Server_CheckModified(object sender, EventArgs e) { Check check = (Check)sender; Task<CheckResult> task = tasks.FirstOrDefault(kvp => kvp.Value == check.Id).Key; if (running) { if (task == null) { // No tasks associated with the check, so schedule a new one tasks.Add(ScheduleExecuteCheckAsync(check), check.Id); } else { // Check was modified or deleted, so remove any waiting tasks CancelCheck(check); if (check.Server != null) { // If the check was not deleted, schedule the new check. // But only if it's still running, otherwise restarting the monitor below // will create a duplicate run. if (running) tasks.Add(ScheduleExecuteCheckAsync(check), check.Id); } } } // Run again in case removing a task above caused it to stop Run(); } private void Server_EnabledChanged(object sender, EventArgs e) { Server server = (Server)sender; if (server.Enabled) { Run(); } else { foreach (Check check in server.Checks) { CancelCheck(check); } } } private void CancelCheck(Check check) { if (tasks == null) return; Task<CheckResult> task = tasks.FirstOrDefault(kvp => kvp.Value == check.Id).Key; if (task != null) tasks.Remove(task); pausedChecks.RemoveAll(id => id == check.Id); if (tokens.TryGetValue(check.Id, out CancellationTokenSource cts)) cts.Cancel(); } private async Task<CheckResult> ScheduleExecuteCheckAsync(Check check) { if (!check.Enabled || !check.Server.Enabled) return await Task.FromResult(new CheckResult(check, CheckStatus.Disabled, null)); CancellationTokenSource cts = new CancellationTokenSource(); tokens[check.Id] = cts; check.NextRunTime = check.Schedule.GetNextTime(check.LastScheduledRunTime); int delay = Math.Max(0, (int)(check.NextRunTime - DateTime.Now).TotalMilliseconds); await Task.Delay(delay, cts.Token); check.LastScheduledRunTime = check.NextRunTime; if (networkAvailable && !cts.IsCancellationRequested) { return await ExecuteCheckAsync(check, cts.Token); } else { if (!pausedChecks.Contains(check.Id)) pausedChecks.Add(check.Id); return await Task.FromResult(new CheckResult(check, CheckStatus.Disabled, null)); } } private void NetworkChange_NetworkAddressChanged(object sender, EventArgs e) { networkAvailable = Helpers.IsNetworkAvailable(); if (networkAvailable) mainForm.Invoke((MethodInvoker)(() => Run())); } private async void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e) { if (e.Mode == PowerModes.Suspend) { foreach (Check check in Checks) { CancelCheck(check); } suspend = true; } else if (e.Mode == PowerModes.Resume) { pausedChecks.Clear(); foreach (Check check in Checks) { //CancelCheck(check); if (check.Enabled && check.Server.Enabled && check.NextRunTime < DateTime.Now) { pausedChecks.Add(check.Id); } } await Task.Delay(10000); suspend = false; Run(); } } private void Application_ApplicationExit(object sender, EventArgs e) { NetworkChange.NetworkAddressChanged -= NetworkChange_NetworkAddressChanged; SystemEvents.PowerModeChanged -= SystemEvents_PowerModeChanged; } public KeyStatus OpenPrivateKey(string path, string password = null) { KeyStatus keyStatus; if (path == null) keyStatus = KeyStatus.NotAccessible; if (privateKeys.TryGetValue(path, out PrivateKeyFile key) && key != null) keyStatus = KeyStatus.Open; else { try { key = new PrivateKeyFile(path, password); keyStatus = KeyStatus.Open; } catch (Exception e) when (e is SshPassPhraseNullOrEmptyException || e is InvalidOperationException) { keyStatus = KeyStatus.NeedPassword; } catch (Exception) { keyStatus = KeyStatus.NotAccessible; } } foreach (Server server in Servers) { if (server.KeyFile == path) { server.PrivateKeyFile = key; server.KeyStatus = keyStatus; } } privateKeys[path] = key; return keyStatus; } private void GenerateIds() { if (Servers.Any()) { int id = Servers.Max(s => s.Id); foreach (Server server in Servers) { if (server.Id == 0) server.Id = ++id; } } if (Checks.Any()) { int id = Math.Max(Settings.Default.MaxCheckId, Checks.Max(c => c.Id)); foreach (Check check in Checks) { if (check.Id == 0) check.Id = ++id; } Settings.Default.MaxCheckId = id; Settings.Default.Save(); } } private XmlSerializer CreateXmlSerializer() { return new XmlSerializer(typeof(List<Server>), Check.CheckTypes); } } public class CheckStatusChangedEventArgs : EventArgs { public Check Check { get; private set; } public CheckResult CheckResult { get; private set; } public CheckStatusChangedEventArgs(Check check, CheckResult result) { Check = check; CheckResult = result; } } public enum FailAction { FlashTaskbar = 0, NotificationBalloon = 1, None = 10 } }