view 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 source

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

    }
}