view ServerMonitor/Objects/Checks/FileCheck.cs @ 25:781d8b980be1

Fix checks not getting scheduled when a server is enabled.
author Brad Greco <brad@bgreco.net>
date Thu, 30 May 2019 21:41:14 -0400
parents 68d7834dc28e
children 88ca7e4fc023
line wrap: on
line source

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text.RegularExpressions;

namespace ServerMonitorApp
{
    /// <summary>Checks file metadata on a remote server.</summary>
    [DisplayName("File check"), Description("Check file size or modified time"), DisplayWeight(12)]
    public class FileCheck : SshCheck
    {
        /// <summary>The command to execute. Must return the output format of GNU ls(1) with the long-iso time style.</summary>
        /// <remarks>Would be better to not rely on the output of ls.</remarks>
        public override string Command => string.Format(FileCommand, Regex.Replace(File, "^~", "$HOME"));

        /// <summary>The command to execute, with a placeholder of {0} for the file to check.</summary>
        protected string FileCommand
        {
            get
            {
                // Invert because POSIX says so.
                int timeZoneOffset = TimeZoneInfo.Local.GetUtcOffset(DateTime.Now).Hours * -1;
                // Set the date format to long-iso since it's easy to parse.
                // Set the time zone to the match the client time zone so comparisons are reliable.
                return "export TIME_STYLE=long-iso; export TZ=UTC" + timeZoneOffset + "; ls -l \"{0}\"";
            }
        }

        /// <summary>The path of the file to check.</summary>
        public string File { get; set; }

        /// <summary>Whether the file size will be checked.</summary>
        public bool CheckFileSize { get; set; }

        /// <summary>Whether the file is expected to be less than or greater than a certain size.</summary>
        public bool FileSizeLessThan { get; set; }

        /// <summary>The size to compare the file against in bytes.</summary>
        public int FileSize { get; set; }

        /// <summary>The size to compare the file against in the selected size units.</summary>
        public double FileSizeInSelectedUnits
        {
            get => Math.Round(ConvertBytesToSelectedUnits(FileSize), 1);
            set => FileSize = ConvertSelectedUnitsToBytes(value);
        }

        /// <summary>The units of the file size.</summary>
        public SizeUnits FileSizeUnits { get; set; }

        /// <summary>Whether the file modified time will be checked.</summary>
        public bool CheckDateModified { get; set; }

        /// <summary>Whether the file is expected to be older than or newer than a certain date.</summary>
        public bool DateModifiedOlderThan { get; set; }

        /// <summary>The number of time units to compare then file modified time against.</summary>
        public double DateModified { get; set; }

        /// <summary>The units of the file date modified.</summary>
        public TimeUnits DateModifiedUnits { get; set; }

        public FileCheck()
        {
            // Set general SSH check settings for file checks.
            CheckExitCode = true;
            ExitCode = 0;
        }

        /// <summary>Processes the output of the file check command.</summary>
        protected override List<CheckResult> ProcessCommandResult(string output, int exitCode)
        {
            // Check for general SSH failures.
            List<CheckResult> results = base.ProcessCommandResult(output, exitCode);
            // If there was an error running the command, fail immediately.
            if (results.Any(r => r.Failed))
                return results;

            // Make sure there is only a single line of output.
            if (output.Split('\n').Length > 1)
            {
                results.Add(Fail("ls output was more than one line: " + output));
            }
            else
            {
                // Split the string into tokens on whitespace.
                string[] tokens = output.Split(new char[0], StringSplitOptions.RemoveEmptyEntries);
                if (CheckFileSize)
                {
                    // Check file size.
                    if (int.TryParse(tokens[4], out int bytes))
                    {
                        // Prepare a human-readable message with the comparison results.
                        string message = string.Format("File size is {0} {1}", Math.Round(ConvertBytesToSelectedUnits(bytes), 1), FileSizeUnits);
                        if ((bytes < FileSize && FileSizeLessThan) || (bytes > FileSize && !FileSizeLessThan))
                            results.Add(Pass(message));
                        else
                            results.Add(Fail(message));
                    }
                    else
                    {
                        results.Add(Fail("Unable to parse ls output as integer: " + tokens[4]));
                    }
                }
                if (CheckDateModified)
                {
                    // Check date modified.
                    // Tokens[5] contains the date, tokens[6] contains the time.
                    string dateString = tokens[5] + " " + tokens[6];
                    if (DateTime.TryParse(dateString, out DateTime modified))
                    {
                        string message = string.Format("Last modified date is {0}", modified);
                        // Determine how old the file is. The command output is in the client time zone.
                        TimeSpan age = DateTime.Now.Subtract(modified);
                        double ageCompare = DateModifiedUnits == TimeUnits.Minute ? age.TotalMinutes :
                                         DateModifiedUnits == TimeUnits.Hour ? age.TotalHours :
                                         age.TotalDays;
                        if ((ageCompare > DateModified && DateModifiedOlderThan) || (ageCompare < DateModified && !DateModifiedOlderThan))
                            results.Add(Pass(message));
                        else
                            results.Add(Fail(message));
                    }
                    else
                    {
                        results.Add(Fail("Unable to parse ls output as date: " + dateString));
                    }
                }
            }
            return results;
        }

        /// <summary>Validates file check options.</summary>
        public override string Validate(bool saving = true)
        {
            string message = base.Validate();
            if (File.IsNullOrEmpty())
                message += "File is required." + Environment.NewLine;
            if (!CheckFileSize && !CheckDateModified)
                message += "At least one check must be enabled." + Environment.NewLine;
            if (CheckFileSize && FileSize < 0)
                message += "File size must be at least 0." + Environment.NewLine;
            if (CheckDateModified && DateModified <= 0)
                message += "Date modified must be greater than 0." + Environment.NewLine;
            return message;
        }

        /// <summary>Converts bytes to a different size unit.</summary>
        /// <param name="sizeInBytes">The size to convert in bytes.</param>
        /// <returns>The size in the units used by this file check.</returns>
        private double ConvertBytesToSelectedUnits(int sizeInBytes)
        {
            return sizeInBytes / Math.Pow(1024, (double)FileSizeUnits);
        }

        /// <summary>Converts a size in a different unit to bytes.</summary>
        /// <param name="sizeInSelectedUnits">The size to convert in the units used by this file check.</param>
        /// <returns>The size in bytes.</returns>
        private int ConvertSelectedUnitsToBytes(double sizeInSelectedUnits)
        {
            return (int)(sizeInSelectedUnits * Math.Pow(1024, (int)FileSizeUnits));
        }
    }
}