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;
+        }
+
+    }
+}