Mercurial > servermonitor
diff ServerMonitor/Objects/ServerMonitor.cs @ 0:3e1a2131f897
Initial commit. Ping check, scheduling, UI working. SSH check mostly working.
author | Brad Greco <brad@bgreco.net> |
---|---|
date | Mon, 31 Dec 2018 18:32:14 -0500 |
parents | |
children | 3142e52cbe69 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ServerMonitor/Objects/ServerMonitor.cs Mon Dec 31 18:32:14 2018 -0500 @@ -0,0 +1,242 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +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 bool running; + private Dictionary<Task<CheckResult>, int> tasks; + //private List<Task<CheckResult>> tasks; + + public event EventHandler<CheckStatusChangedEventArgs> CheckStatusChanged; + + public List<Server> Servers { get; private set; } = new List<Server>(); + + public IEnumerable<Check> Checks { get { return Servers.SelectMany(s => s.Checks); } } + + public string ConfigFile { get; private set; } + + public ServerMonitor() + { + 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); + } + + public void LoadServers() + { + 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) + { + foreach (Check check in server.Checks) + { + check.Server = server; + } + server.CheckModified += Server_CheckModified; + } + } + // If the file doesn't exist, no special handling is needed. It will be created later. + catch (FileNotFoundException) { } + catch (DirectoryNotFoundException) { } + catch (InvalidOperationException) + { + //TODO log + throw; + } + finally + { + reader?.Close(); + } + Run(); + } + + public void SaveServers() + { + GenerateIds(); + TextWriter writer = null; + XmlSerializer serializer = null; + 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); + } + catch (Exception) + { + //TODO log + throw; + } + finally + { + writer?.Close(); + } + } + + private async void Run() + { + if (running) + return; + running = true; + //TODO subscribe to power events. Find any check's NextExecutionTime is in the past. Cancel waiting task and run immediately (or after short delay). + //tasks = Checks.Select(c => ScheduleExecuteCheckAsync(c)).ToList(); + 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); + OnCheckStatusChanged(check, result); + logger.Log(result); + return 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 + tasks.Remove(task); + if (tokens.TryGetValue(check.Id, out CancellationTokenSource cts)) + cts.Cancel(); + 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 async Task<CheckResult> ScheduleExecuteCheckAsync(Check check) + { + if (!check.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); + await Task.Delay(check.NextRunTime - DateTime.Now, cts.Token); + check.LastScheduledRunTime = check.NextRunTime; + return await ExecuteCheckAsync(check, cts.Token); + } + + 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; + } + } + + //TODO if a check is deleted, there might be old results in the log file that share an ID with a new one + if (Checks.Any()) + { + int id = Checks.Max(c => c.Id); + foreach (Check check in Checks) + { + if (check.Id == 0) + check.Id = ++id; + } + } + } + + 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; + } + + } +}