changeset 2:453ecc1ed9ea

Disk space check
author Brad Greco <brad@bgreco.net>
date Sun, 06 Jan 2019 20:49:08 -0500
parents 9e92780ebc0f
children 96f0b028176d
files ServerMonitor/Controls/CheckControl.cs ServerMonitor/Controls/DiskSpaceCheckControl.Designer.cs ServerMonitor/Controls/DiskSpaceCheckControl.cs ServerMonitor/Controls/DiskSpaceCheckControl.resx ServerMonitor/Helpers.cs ServerMonitor/Objects/Check.cs ServerMonitor/Objects/CheckResult.cs ServerMonitor/Objects/Checks/Check.cs ServerMonitor/Objects/Checks/DiskSpaceCheck.cs ServerMonitor/Objects/Checks/HttpCheck.cs ServerMonitor/Objects/Checks/PingCheck.cs ServerMonitor/Objects/Checks/SshCheck.cs ServerMonitor/Objects/HttpCheck.cs ServerMonitor/Objects/PingCheck.cs ServerMonitor/Objects/SshCheck.cs ServerMonitor/ServerMonitor.csproj
diffstat 16 files changed, 965 insertions(+), 539 deletions(-) [+]
line wrap: on
line diff
--- a/ServerMonitor/Controls/CheckControl.cs	Tue Jan 01 21:14:47 2019 -0500
+++ b/ServerMonitor/Controls/CheckControl.cs	Sun Jan 06 20:49:08 2019 -0500
@@ -27,9 +27,12 @@
             IEnumerable<Panel> panels = CheckGroupBox.Controls.OfType<Panel>();
             foreach (Panel panel in panels)
             {
-                CheckBox mainCheckBox = panel.Controls.OfType<CheckBox>().OrderBy(c => c.Left).First();
-                mainCheckBox.CheckedChanged += CheckControl_CheckedChanged;
-                DisablePanelByCheckBox(mainCheckBox);
+                CheckBox mainCheckBox = panel.Controls.OfType<CheckBox>().OrderBy(c => c.Left).FirstOrDefault();
+                if (mainCheckBox != null)
+                {
+                    mainCheckBox.CheckedChanged += CheckControl_CheckedChanged;
+                    DisablePanelByCheckBox(mainCheckBox);
+                }
             }
         }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ServerMonitor/Controls/DiskSpaceCheckControl.Designer.cs	Sun Jan 06 20:49:08 2019 -0500
@@ -0,0 +1,135 @@
+namespace ServerMonitorApp
+{
+    partial class DiskSpaceCheckControl
+    {
+        /// <summary> 
+        /// Required designer variable.
+        /// </summary>
+        private System.ComponentModel.IContainer components = null;
+
+        /// <summary> 
+        /// Clean up any resources being used.
+        /// </summary>
+        /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
+        protected override void Dispose(bool disposing)
+        {
+            if (disposing && (components != null))
+            {
+                components.Dispose();
+            }
+            base.Dispose(disposing);
+        }
+
+        #region Component Designer generated code
+
+        /// <summary> 
+        /// Required method for Designer support - do not modify 
+        /// the contents of this method with the code editor.
+        /// </summary>
+        private void InitializeComponent()
+        {
+            this.ResponseBodyPanel = new System.Windows.Forms.Panel();
+            this.FreeSpaceUnitsComboBox = new System.Windows.Forms.ComboBox();
+            this.FreeSpaceLabel = new System.Windows.Forms.Label();
+            this.FreeSpaceTextBox = new System.Windows.Forms.TextBox();
+            this.DeviceLabel = new System.Windows.Forms.Label();
+            this.DeviceTextBox = new System.Windows.Forms.TextBox();
+            this.CheckGroupBox.SuspendLayout();
+            this.ResponseBodyPanel.SuspendLayout();
+            this.SuspendLayout();
+            // 
+            // CheckGroupBox
+            // 
+            this.CheckGroupBox.Controls.Add(this.ResponseBodyPanel);
+            this.CheckGroupBox.Controls.Add(this.DeviceLabel);
+            this.CheckGroupBox.Controls.Add(this.DeviceTextBox);
+            this.CheckGroupBox.Size = new System.Drawing.Size(526, 87);
+            this.CheckGroupBox.Text = "null";
+            // 
+            // ResponseBodyPanel
+            // 
+            this.ResponseBodyPanel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) 
+            | System.Windows.Forms.AnchorStyles.Right)));
+            this.ResponseBodyPanel.Controls.Add(this.FreeSpaceUnitsComboBox);
+            this.ResponseBodyPanel.Controls.Add(this.FreeSpaceLabel);
+            this.ResponseBodyPanel.Controls.Add(this.FreeSpaceTextBox);
+            this.ResponseBodyPanel.Location = new System.Drawing.Point(9, 48);
+            this.ResponseBodyPanel.Name = "ResponseBodyPanel";
+            this.ResponseBodyPanel.Size = new System.Drawing.Size(511, 28);
+            this.ResponseBodyPanel.TabIndex = 21;
+            // 
+            // FreeSpaceUnitsComboBox
+            // 
+            this.FreeSpaceUnitsComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
+            this.FreeSpaceUnitsComboBox.FormattingEnabled = true;
+            this.FreeSpaceUnitsComboBox.Items.AddRange(new object[] {
+            "MB",
+            "GB",
+            "percent"});
+            this.FreeSpaceUnitsComboBox.Location = new System.Drawing.Point(188, 4);
+            this.FreeSpaceUnitsComboBox.Name = "FreeSpaceUnitsComboBox";
+            this.FreeSpaceUnitsComboBox.Size = new System.Drawing.Size(64, 21);
+            this.FreeSpaceUnitsComboBox.TabIndex = 23;
+            // 
+            // FreeSpaceLabel
+            // 
+            this.FreeSpaceLabel.AutoSize = true;
+            this.FreeSpaceLabel.Location = new System.Drawing.Point(-3, 7);
+            this.FreeSpaceLabel.Name = "FreeSpaceLabel";
+            this.FreeSpaceLabel.Size = new System.Drawing.Size(110, 13);
+            this.FreeSpaceLabel.TabIndex = 22;
+            this.FreeSpaceLabel.Text = "Free space is at least:";
+            // 
+            // FreeSpaceTextBox
+            // 
+            this.FreeSpaceTextBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) 
+            | System.Windows.Forms.AnchorStyles.Right)));
+            this.FreeSpaceTextBox.Location = new System.Drawing.Point(113, 4);
+            this.FreeSpaceTextBox.Name = "FreeSpaceTextBox";
+            this.FreeSpaceTextBox.Size = new System.Drawing.Size(69, 20);
+            this.FreeSpaceTextBox.TabIndex = 7;
+            // 
+            // DeviceLabel
+            // 
+            this.DeviceLabel.AutoSize = true;
+            this.DeviceLabel.Location = new System.Drawing.Point(6, 25);
+            this.DeviceLabel.Name = "DeviceLabel";
+            this.DeviceLabel.Size = new System.Drawing.Size(69, 13);
+            this.DeviceLabel.TabIndex = 18;
+            this.DeviceLabel.Text = "File / device:";
+            // 
+            // DeviceTextBox
+            // 
+            this.DeviceTextBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) 
+            | System.Windows.Forms.AnchorStyles.Right)));
+            this.DeviceTextBox.Location = new System.Drawing.Point(77, 22);
+            this.DeviceTextBox.Name = "DeviceTextBox";
+            this.DeviceTextBox.Size = new System.Drawing.Size(443, 20);
+            this.DeviceTextBox.TabIndex = 17;
+            this.DeviceTextBox.Text = "/";
+            // 
+            // DiskSpaceCheckControl
+            // 
+            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
+            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+            this.Name = "DiskSpaceCheckControl";
+            this.Size = new System.Drawing.Size(526, 87);
+            this.Load += new System.EventHandler(this.DiskSpaceCheckControl_Load);
+            this.CheckGroupBox.ResumeLayout(false);
+            this.CheckGroupBox.PerformLayout();
+            this.ResponseBodyPanel.ResumeLayout(false);
+            this.ResponseBodyPanel.PerformLayout();
+            this.ResumeLayout(false);
+
+        }
+
+        #endregion
+
+        private System.Windows.Forms.Panel ResponseBodyPanel;
+        private System.Windows.Forms.TextBox FreeSpaceTextBox;
+        private System.Windows.Forms.Label DeviceLabel;
+        private System.Windows.Forms.TextBox DeviceTextBox;
+        private System.Windows.Forms.ComboBox FreeSpaceUnitsComboBox;
+        private System.Windows.Forms.Label FreeSpaceLabel;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ServerMonitor/Controls/DiskSpaceCheckControl.cs	Sun Jan 06 20:49:08 2019 -0500
@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Drawing;
+using System.Data;
+using System.Linq;
+using System.Text;
+using System.Windows.Forms;
+
+namespace ServerMonitorApp
+{
+    [CheckType(typeof(DiskSpaceCheck))]
+    public partial class DiskSpaceCheckControl : CheckControl
+    {
+        public DiskSpaceCheckControl()
+        {
+            InitializeComponent();
+        }
+
+        private void DiskSpaceCheckControl_Load(object sender, EventArgs e)
+        {
+            FreeSpaceUnitsComboBox.SelectedIndex = 1;
+        }
+
+        public override void LoadCheck(Check check1)
+        {
+            DiskSpaceCheck check = (DiskSpaceCheck)check1;
+            DeviceTextBox.Text = check.Device;
+            FreeSpaceTextBox.Text = check.MinFreeSpace.ToString();
+            FreeSpaceUnitsComboBox.SelectedIndex = (int)check.FreeSpaceUnits;
+        }
+
+        public override void UpdateCheck(Check check1)
+        {
+            DiskSpaceCheck check = (DiskSpaceCheck)check1;
+            check.Device = DeviceTextBox.Text.Trim();
+            check.FreeSpaceUnits = (FreeSpaceUnits)FreeSpaceUnitsComboBox.SelectedIndex;
+            try
+            {
+                check.MinFreeSpace = double.Parse(FreeSpaceTextBox.Text);
+            }
+            catch
+            {
+                check.MinFreeSpace = 0;
+                throw new UpdateCheckException("Free space must be numeric.");
+            }
+        }
+    }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ServerMonitor/Controls/DiskSpaceCheckControl.resx	Sun Jan 06 20:49:08 2019 -0500
@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+  <!-- 
+    Microsoft ResX Schema 
+    
+    Version 2.0
+    
+    The primary goals of this format is to allow a simple XML format 
+    that is mostly human readable. The generation and parsing of the 
+    various data types are done through the TypeConverter classes 
+    associated with the data types.
+    
+    Example:
+    
+    ... ado.net/XML headers & schema ...
+    <resheader name="resmimetype">text/microsoft-resx</resheader>
+    <resheader name="version">2.0</resheader>
+    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+        <value>[base64 mime encoded serialized .NET Framework object]</value>
+    </data>
+    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+        <comment>This is a comment</comment>
+    </data>
+                
+    There are any number of "resheader" rows that contain simple 
+    name/value pairs.
+    
+    Each data row contains a name, and value. The row also contains a 
+    type or mimetype. Type corresponds to a .NET class that support 
+    text/value conversion through the TypeConverter architecture. 
+    Classes that don't support this are serialized and stored with the 
+    mimetype set.
+    
+    The mimetype is used for serialized objects, and tells the 
+    ResXResourceReader how to depersist the object. This is currently not 
+    extensible. For a given mimetype the value must be set accordingly:
+    
+    Note - application/x-microsoft.net.object.binary.base64 is the format 
+    that the ResXResourceWriter will generate, however the reader can 
+    read any of the formats listed below.
+    
+    mimetype: application/x-microsoft.net.object.binary.base64
+    value   : The object must be serialized with 
+            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+            : and then encoded with base64 encoding.
+    
+    mimetype: application/x-microsoft.net.object.soap.base64
+    value   : The object must be serialized with 
+            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+            : and then encoded with base64 encoding.
+
+    mimetype: application/x-microsoft.net.object.bytearray.base64
+    value   : The object must be serialized into a byte array 
+            : using a System.ComponentModel.TypeConverter
+            : and then encoded with base64 encoding.
+    -->
+  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+    <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+    <xsd:element name="root" msdata:IsDataSet="true">
+      <xsd:complexType>
+        <xsd:choice maxOccurs="unbounded">
+          <xsd:element name="metadata">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" />
+              </xsd:sequence>
+              <xsd:attribute name="name" use="required" type="xsd:string" />
+              <xsd:attribute name="type" type="xsd:string" />
+              <xsd:attribute name="mimetype" type="xsd:string" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="assembly">
+            <xsd:complexType>
+              <xsd:attribute name="alias" type="xsd:string" />
+              <xsd:attribute name="name" type="xsd:string" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="data">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="resheader">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" />
+            </xsd:complexType>
+          </xsd:element>
+        </xsd:choice>
+      </xsd:complexType>
+    </xsd:element>
+  </xsd:schema>
+  <resheader name="resmimetype">
+    <value>text/microsoft-resx</value>
+  </resheader>
+  <resheader name="version">
+    <value>2.0</value>
+  </resheader>
+  <resheader name="reader">
+    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <resheader name="writer">
+    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+</root>
\ No newline at end of file
--- a/ServerMonitor/Helpers.cs	Tue Jan 01 21:14:47 2019 -0500
+++ b/ServerMonitor/Helpers.cs	Sun Jan 06 20:49:08 2019 -0500
@@ -31,6 +31,11 @@
             return aString == null || aString.Trim() == string.Empty;
         }
 
+        public static string ConvertNewlines(this string aString)
+        {
+            return aString.Replace("\r\n", "\n").Replace('\r', '\n');
+        }
+
         public static T GetAttribute<T>(this Type type) where T : Attribute
         {
             return type.GetCustomAttributes(typeof(T), false).SingleOrDefault() as T;
--- a/ServerMonitor/Objects/Check.cs	Tue Jan 01 21:14:47 2019 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,234 +0,0 @@
-using System;
-using System.Linq;
-using System.Text;
-using System.Text.RegularExpressions;
-using System.Threading;
-using System.Threading.Tasks;
-using System.Xml.Serialization;
-
-namespace ServerMonitorApp
-{
-    /*public enum CheckType
-    {
-        Command
-    }*/
-
-    public enum CheckStatus
-    {
-        Success,
-        Information,
-        Warning,
-        Error,
-        Running,
-        Disabled,
-    }
-
-    public abstract class Check
-    {
-        private static Type[] _checkTypes;
-
-        public static Type[] CheckTypes
-        {
-            get
-            {
-                return _checkTypes ?? (_checkTypes = typeof(Check).Assembly.GetTypes()
-                    .Where(t => t.IsSubclassOf(typeof(Check)))
-                    .OrderBy(t => t.GetAttribute<DisplayWeightAttribute>()?.DisplayWeight).ToArray());
-            }
-        }
-
-        public int Id { get; set; }
-
-        public string Name { get; set; }
-
-        /*public CheckType Type { get; set; }*/
-
-        public int Timeout { get; set; }
-
-        public bool Enabled { get; set; }
-
-        public Schedule Schedule { get; set; }
-
-        public DateTime LastRunTime { get; set; }
-
-        public DateTime LastScheduledRunTime { get; set; }
-
-        public DateTime NextRunTime { get; set; }
-
-        public string LastMessage { get; set; }
-
-        public CheckStatus Status { get; set; }
-
-        public CheckStatus FailStatus { get; set; }
-
-        [XmlIgnore]
-        public Server Server { get; set; }
-
-        public Check()
-        {
-            FailStatus = CheckStatus.Error;
-        }
-
-        public override string ToString()
-        {
-            return Name;
-        }
-
-        public virtual string Validate(bool saving = true)
-        {
-            string message = string.Empty;
-            if (Name.IsNullOrEmpty() && saving)
-                message += "Name cannot be blank." + Environment.NewLine;
-            return message;
-        }
-
-        //public virtual CheckStatus Execute()
-        //{
-        //    //TODO
-        //    throw new NotImplementedException();
-        //}
-
-        public async Task<CheckResult> ExecuteAsync(CancellationToken token = default(CancellationToken), bool update = true)
-        {
-            //TODO check cancellation token before proceeding
-            CheckResult result;
-            DateTime startTime = DateTime.Now;
-            try
-            {
-                Task<CheckResult> checkTask = ExecuteCheckAsync(token);
-                try
-                {
-                    if (await Task.WhenAny(checkTask, Task.Delay(Timeout, token)) == checkTask)
-                    {
-                        result = await checkTask;
-                    }
-                    else
-                    {
-                        result = Fail("Timed out.");
-                    }
-                }
-                catch (TaskCanceledException)
-                {
-                    return null;
-                }
-            }
-            catch (Exception e)
-            {
-                result = Fail(e.GetBaseException().Message);
-            }
-            result.StartTime = startTime;
-            result.EndTime = DateTime.Now;
-            // If a check is executed from the CheckForm, we don't want to update the status or log the event.
-            if (update)
-            {
-                Status = result.CheckStatus;
-                LastMessage = result.Message;
-                LastRunTime = result.EndTime;
-            }
-            return result;
-        }
-
-        protected CheckResult Pass(string message)
-        {
-            return new CheckResult(this, CheckStatus.Success, message);
-        }
-
-        protected CheckResult Fail(string message)
-        {
-            return new CheckResult(this, FailStatus, message);
-        }
-
-        protected CheckResult Fail(Exception e)
-        {
-            return new CheckResult(this, FailStatus, e.GetBaseException().Message);
-        }
-
-        protected virtual CheckResult GetIntResult(int expectedValue, int resultValue, string description)
-        {
-            if (expectedValue == resultValue)
-                return Pass(string.Format("{0}: {1}", description, resultValue));
-            else
-                return Fail(string.Format("{0}: {1} (expected: {2})", description, resultValue, expectedValue));
-        }
-
-        protected virtual CheckResult GetStringResult(MatchType matchType, string expectedPattern, bool useRegex, string resultValue, string description)
-        {
-            bool match;
-            if (useRegex)
-            {
-                if (matchType.In(MatchType.Equals, MatchType.NotEquals))
-                {
-                    if (!expectedPattern.StartsWith("^"))
-                        expectedPattern = "^" + expectedPattern;
-                    if (!expectedPattern.EndsWith("$"))
-                        expectedPattern += "$";
-                }
-                Regex re = new Regex(expectedPattern, RegexOptions.Singleline);
-                match = re.IsMatch(resultValue);
-            }
-            else
-            {
-                if (matchType.In(MatchType.Equals, MatchType.NotEquals))
-                {
-                    match = expectedPattern == resultValue;
-                }
-                else if (matchType.In(MatchType.Contains, MatchType.NotContains))
-                {
-                    match = resultValue.Contains(expectedPattern);
-                }
-                else
-                {
-                    if (decimal.TryParse(expectedPattern, out decimal expectedNumeric) &&
-                        decimal.TryParse(resultValue, out decimal resultNumeric))
-                    {
-                        match = (matchType == MatchType.GreaterThan && resultNumeric > expectedNumeric) ||
-                                (matchType == MatchType.LessThan    && resultNumeric < expectedNumeric);
-                    }
-                    else
-                    {
-                        return Fail(string.Format("{0} is not numeric: {1}", description, resultValue));
-                    }
-                }
-            }
-
-            if (matchType.In(MatchType.Equals, MatchType.Contains))
-            {
-                if (match)
-                    return Pass(string.Format("{0} {1} the pattern: {2}", description, matchType.ToString().ToLower(), expectedPattern));
-                else
-                    return Fail(string.Format("{0} does not {1} the pattern: {2} ({0}: {3})", description, matchType.ToString().ToLower().TrimEnd('s'), expectedPattern, resultValue));
-            }
-            else if (matchType.In(MatchType.NotEquals, MatchType.NotContains))
-            {
-                if (match)
-                    return Fail(string.Format("{0} {1} the pattern: {2} ({0}: {3})", description, matchType.ToString().ToLower().Replace("not", ""), expectedPattern, resultValue));
-                else
-                    return Pass(string.Format("{0} does not {1} the pattern: {2}", description, matchType.ToString().ToLower().TrimEnd('s').Replace("not", ""), expectedPattern));
-            }
-            else
-            {
-                if (match)
-                    return Pass(string.Format("{0} ({1}) is {2} {3}", description, resultValue, matchType.ToString().ToLower().Replace("than", " than"), expectedPattern));
-                else
-                    return Fail(string.Format("{0} ({1}) is not {2} {3}", description, resultValue, matchType.ToString().ToLower().Replace("than", " than"), expectedPattern));
-            }
-        }
-
-        protected CheckResult MergeResults(params CheckResult[] results)
-        {
-            StringBuilder message = new StringBuilder();
-            bool failed = false;
-            foreach (CheckResult result in results)
-            {
-                if (result == null)
-                    continue;
-                if (result.CheckStatus != CheckStatus.Success)
-                    failed = true;
-                message.AppendLine(result.Message);
-            }
-            return failed ? Fail(message.ToString().Trim()) : Pass(message.ToString().Trim());
-        }
-
-        protected abstract Task<CheckResult> ExecuteCheckAsync(CancellationToken token);
-    }
-}
\ No newline at end of file
--- a/ServerMonitor/Objects/CheckResult.cs	Tue Jan 01 21:14:47 2019 -0500
+++ b/ServerMonitor/Objects/CheckResult.cs	Sun Jan 06 20:49:08 2019 -0500
@@ -32,7 +32,7 @@
                 StartTime.ToString(dateFormat).Replace("T", " "),
                 EndTime.ToString(dateFormat).Replace("T", " "),
                 CheckStatus,
-                Message.Replace("\r\n", "\n").Replace('\r', '\n').Replace("\n", "\\n"));
+                Message.ConvertNewlines().Replace("\n", "\\n"));
         }
 
         public static CheckResult FromLogString(Check check, string logString)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ServerMonitor/Objects/Checks/Check.cs	Sun Jan 06 20:49:08 2019 -0500
@@ -0,0 +1,234 @@
+using System;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml.Serialization;
+
+namespace ServerMonitorApp
+{
+    /*public enum CheckType
+    {
+        Command
+    }*/
+
+    public enum CheckStatus
+    {
+        Success,
+        Information,
+        Warning,
+        Error,
+        Running,
+        Disabled,
+    }
+
+    public abstract class Check
+    {
+        private static Type[] _checkTypes;
+
+        public static Type[] CheckTypes
+        {
+            get
+            {
+                return _checkTypes ?? (_checkTypes = typeof(Check).Assembly.GetTypes()
+                    .Where(t => t.IsSubclassOf(typeof(Check)))
+                    .OrderBy(t => t.GetAttribute<DisplayWeightAttribute>()?.DisplayWeight).ToArray());
+            }
+        }
+
+        public int Id { get; set; }
+
+        public string Name { get; set; }
+
+        /*public CheckType Type { get; set; }*/
+
+        public int Timeout { get; set; }
+
+        public bool Enabled { get; set; }
+
+        public Schedule Schedule { get; set; }
+
+        public DateTime LastRunTime { get; set; }
+
+        public DateTime LastScheduledRunTime { get; set; }
+
+        public DateTime NextRunTime { get; set; }
+
+        public string LastMessage { get; set; }
+
+        public CheckStatus Status { get; set; }
+
+        public CheckStatus FailStatus { get; set; }
+
+        [XmlIgnore]
+        public Server Server { get; set; }
+
+        public Check()
+        {
+            FailStatus = CheckStatus.Error;
+        }
+
+        public override string ToString()
+        {
+            return Name;
+        }
+
+        public virtual string Validate(bool saving = true)
+        {
+            string message = string.Empty;
+            if (Name.IsNullOrEmpty() && saving)
+                message += "Name cannot be blank." + Environment.NewLine;
+            return message;
+        }
+
+        //public virtual CheckStatus Execute()
+        //{
+        //    //TODO
+        //    throw new NotImplementedException();
+        //}
+
+        public async Task<CheckResult> ExecuteAsync(CancellationToken token = default(CancellationToken), bool update = true)
+        {
+            //TODO check cancellation token before proceeding
+            CheckResult result;
+            DateTime startTime = DateTime.Now;
+            try
+            {
+                Task<CheckResult> checkTask = ExecuteCheckAsync(token);
+                try
+                {
+                    if (await Task.WhenAny(checkTask, Task.Delay(Timeout, token)) == checkTask)
+                    {
+                        result = await checkTask;
+                    }
+                    else
+                    {
+                        result = Fail("Timed out.");
+                    }
+                }
+                catch (TaskCanceledException)
+                {
+                    return null;
+                }
+            }
+            catch (Exception e)
+            {
+                result = Fail(e.GetBaseException().Message);
+            }
+            result.StartTime = startTime;
+            result.EndTime = DateTime.Now;
+            // If a check is executed from the CheckForm, we don't want to update the status or log the event.
+            if (update)
+            {
+                Status = result.CheckStatus;
+                LastMessage = result.Message;
+                LastRunTime = result.EndTime;
+            }
+            return result;
+        }
+
+        public CheckResult Pass(string message)
+        {
+            return new CheckResult(this, CheckStatus.Success, message);
+        }
+
+        public CheckResult Fail(string message)
+        {
+            return new CheckResult(this, FailStatus, message);
+        }
+
+        protected CheckResult Fail(Exception e)
+        {
+            return new CheckResult(this, FailStatus, e.GetBaseException().Message);
+        }
+
+        protected CheckResult GetIntResult(int expectedValue, int resultValue, string description)
+        {
+            if (expectedValue == resultValue)
+                return Pass(string.Format("{0}: {1}", description, resultValue));
+            else
+                return Fail(string.Format("{0}: {1} (expected: {2})", description, resultValue, expectedValue));
+        }
+
+        protected CheckResult GetStringResult(MatchType matchType, string expectedPattern, bool useRegex, string resultValue, string description)
+        {
+            bool match;
+            if (useRegex)
+            {
+                if (matchType.In(MatchType.Equals, MatchType.NotEquals))
+                {
+                    if (!expectedPattern.StartsWith("^"))
+                        expectedPattern = "^" + expectedPattern;
+                    if (!expectedPattern.EndsWith("$"))
+                        expectedPattern += "$";
+                }
+                Regex re = new Regex(expectedPattern, RegexOptions.Singleline);
+                match = re.IsMatch(resultValue);
+            }
+            else
+            {
+                if (matchType.In(MatchType.Equals, MatchType.NotEquals))
+                {
+                    match = expectedPattern == resultValue;
+                }
+                else if (matchType.In(MatchType.Contains, MatchType.NotContains))
+                {
+                    match = resultValue.Contains(expectedPattern);
+                }
+                else
+                {
+                    if (decimal.TryParse(expectedPattern, out decimal expectedNumeric) &&
+                        decimal.TryParse(resultValue, out decimal resultNumeric))
+                    {
+                        match = (matchType == MatchType.GreaterThan && resultNumeric > expectedNumeric) ||
+                                (matchType == MatchType.LessThan    && resultNumeric < expectedNumeric);
+                    }
+                    else
+                    {
+                        return Fail(string.Format("{0} is not numeric: {1}", description, resultValue));
+                    }
+                }
+            }
+
+            if (matchType.In(MatchType.Equals, MatchType.Contains))
+            {
+                if (match)
+                    return Pass(string.Format("{0} {1} the pattern: {2}", description, matchType.ToString().ToLower(), expectedPattern));
+                else
+                    return Fail(string.Format("{0} does not {1} the pattern: {2} ({0}: {3})", description, matchType.ToString().ToLower().TrimEnd('s'), expectedPattern, resultValue));
+            }
+            else if (matchType.In(MatchType.NotEquals, MatchType.NotContains))
+            {
+                if (match)
+                    return Fail(string.Format("{0} {1} the pattern: {2} ({0}: {3})", description, matchType.ToString().ToLower().Replace("not", ""), expectedPattern, resultValue));
+                else
+                    return Pass(string.Format("{0} does not {1} the pattern: {2}", description, matchType.ToString().ToLower().TrimEnd('s').Replace("not", ""), expectedPattern));
+            }
+            else
+            {
+                if (match)
+                    return Pass(string.Format("{0} ({1}) is {2} {3}", description, resultValue, matchType.ToString().ToLower().Replace("than", " than"), expectedPattern));
+                else
+                    return Fail(string.Format("{0} ({1}) is not {2} {3}", description, resultValue, matchType.ToString().ToLower().Replace("than", " than"), expectedPattern));
+            }
+        }
+
+        protected CheckResult MergeResults(params CheckResult[] results)
+        {
+            StringBuilder message = new StringBuilder();
+            bool failed = false;
+            foreach (CheckResult result in results)
+            {
+                if (result == null)
+                    continue;
+                if (result.CheckStatus != CheckStatus.Success)
+                    failed = true;
+                message.AppendLine(result.Message);
+            }
+            return failed ? Fail(message.ToString().Trim()) : Pass(message.ToString().Trim());
+        }
+
+        protected abstract Task<CheckResult> ExecuteCheckAsync(CancellationToken token);
+    }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ServerMonitor/Objects/Checks/DiskSpaceCheck.cs	Sun Jan 06 20:49:08 2019 -0500
@@ -0,0 +1,94 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace ServerMonitorApp
+{
+    [DisplayName("Disk space check"), Description("Check the remaining free disk space"), DisplayWeight(11)]
+    public class DiskSpaceCheck : SshCheck
+    {
+        public string Device { get; set; }
+
+        public double MinFreeSpace { get; set; }
+
+        public FreeSpaceUnits FreeSpaceUnits { get; set; }
+
+        public DiskSpaceCheck()
+        {
+            Command = "df -P -k {0} | awk 'NR>1' | tr -s ' ' | cut -d ' ' -f 4,5";
+            CheckExitCode = true;
+            ExitCode = 0;
+        }
+
+        protected override string GetCommand()
+        {
+            return string.Format(base.GetCommand(), Device);
+        }
+
+        protected override List<CheckResult> ProcessCommandResult(string output, int exitCode)
+        {
+            List<CheckResult> results = base.ProcessCommandResult(output, exitCode);
+            if (output.Split('\n').Length > 1)
+            {
+                results.Add(Fail("df output was more than one line: " + output));
+            }
+            else
+            {
+
+                string[] tokens = output.Split(new char[0], StringSplitOptions.RemoveEmptyEntries);
+                if (FreeSpaceUnits == FreeSpaceUnits.percent)
+                {
+                    if (int.TryParse(tokens[1].Replace("%", ""), out int percent))
+                    {
+                        percent = 100 - percent;
+                        string message = string.Format("Free disk space is {0}%", percent);
+                        if (percent < MinFreeSpace)
+                            results.Add(Fail(message));
+                        else
+                            results.Add(Pass(message));
+                    }
+                    else
+                    {
+                        results.Add(Fail("Unable to parse df output as integer: " + tokens[1].Replace("%", "")));
+                    }
+                }
+                else
+                {
+                    if (int.TryParse(tokens[0], out int freeSpace))
+                    {
+                        freeSpace /= 1024;
+                        if (FreeSpaceUnits == FreeSpaceUnits.GB)
+                            freeSpace /= 1024;
+                        string message = string.Format("Free disk space is {0} {1}", freeSpace, FreeSpaceUnits);
+                        if (freeSpace < MinFreeSpace)
+                            results.Add(Fail(message));
+                        else
+                            results.Add(Pass(message));
+                    }
+                    else
+                    {
+                        results.Add(Fail("Unable to parse df output as integer: " + tokens[0]));
+                    }
+                }
+            }
+            return results;
+        }
+
+        public override string Validate(bool saving = true)
+        {
+            string message = base.Validate();
+            if (Device.IsNullOrEmpty())
+                message += "Device is required." + Environment.NewLine;
+            if (MinFreeSpace <= 0)
+                message += "Free space must be greater than 0." + Environment.NewLine;
+            else if (FreeSpaceUnits == FreeSpaceUnits.percent && MinFreeSpace > 100)
+                message += "Free space percent must be between 0 and 100." + Environment.NewLine;
+            return message;
+        }
+    }
+
+    public enum FreeSpaceUnits { MB = 0, GB = 1, percent = 2 }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ServerMonitor/Objects/Checks/HttpCheck.cs	Sun Jan 06 20:49:08 2019 -0500
@@ -0,0 +1,135 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace ServerMonitorApp
+{
+    [DisplayName("HTTP check"), Description("Check the result of an HTTP request"), DisplayWeight(1)]
+    public class HttpCheck : Check
+    {
+        public string Url { get; set; }
+
+        public bool CheckResponseCode { get; set; }
+
+        public int ResponseCode { get; set; }
+
+        public bool CheckResponseLength { get; set; }
+
+        public string ResponseLengthMin { get; set; }
+
+        public string ResponseLengthMax { get; set; }
+
+        public bool CheckResponseBody { get; set; }
+
+        public MatchType ResponseBodyMatchType { get; set; }
+
+        public string ResponseBodyPattern { get; set; }
+
+        public bool ResponseBodyUseRegex { get; set; }
+
+        protected override Task<CheckResult> ExecuteCheckAsync(CancellationToken token)
+        {
+            throw new NotImplementedException();
+        }
+
+        public override string Validate(bool saving = true)
+        {
+            string message = base.Validate();
+            if (Url.IsNullOrEmpty())
+                message += "URL cannot be blank." + Environment.NewLine;
+            if (!CheckResponseCode && !CheckResponseLength && !CheckResponseBody)
+                message += "At least one check must be enabled." + Environment.NewLine;
+            if (CheckResponseBody && ResponseBodyUseRegex)
+            {
+                try
+                {
+                    Regex re = new Regex(ResponseBodyPattern);
+                }
+                catch (ArgumentException)
+                {
+                    message += "Invalid regular expression for response body." + Environment.NewLine;
+                }
+            }
+            return message;
+        }
+
+        //protected override CheckResult GetIntResult(int expectedValue, int resultValue, string description)
+        //{
+        //    CheckResult result = base.GetIntResult(expectedValue, resultValue, description);
+
+        //}
+
+/*
+100   Continue[RFC7231, Section 6.2.1]
+101   Switching Protocols[RFC7231, Section 6.2.2]
+102   Processing[RFC2518]
+103   Early Hints[RFC8297]
+200   OK[RFC7231, Section 6.3.1]
+201   Created[RFC7231, Section 6.3.2]
+202   Accepted[RFC7231, Section 6.3.3]
+203   Non-Authoritative Information[RFC7231, Section 6.3.4]
+204   No Content[RFC7231, Section 6.3.5]
+205   Reset Content[RFC7231, Section 6.3.6]
+206   Partial Content[RFC7233, Section 4.1]
+207   Multi-Status[RFC4918]
+208   Already Reported[RFC5842]
+226   IM Used[RFC3229]
+300   Multiple Choices[RFC7231, Section 6.4.1]
+301   Moved Permanently[RFC7231, Section 6.4.2]
+302   Found[RFC7231, Section 6.4.3]
+303   See Other[RFC7231, Section 6.4.4]
+304   Not Modified[RFC7232, Section 4.1]
+305   Use Proxy[RFC7231, Section 6.4.5]
+306   (Unused)[RFC7231, Section 6.4.6]
+307   Temporary Redirect[RFC7231, Section 6.4.7]
+308   Permanent Redirect[RFC7538]
+400   Bad Request[RFC7231, Section 6.5.1]
+401   Unauthorized[RFC7235, Section 3.1]
+402   Payment Required[RFC7231, Section 6.5.2]
+403   Forbidden[RFC7231, Section 6.5.3]
+404   Not Found[RFC7231, Section 6.5.4]
+405   Method Not Allowed[RFC7231, Section 6.5.5]
+406   Not Acceptable[RFC7231, Section 6.5.6]
+407   Proxy Authentication Required[RFC7235, Section 3.2]
+408   Request Timeout[RFC7231, Section 6.5.7]
+409   Conflict[RFC7231, Section 6.5.8]
+410   Gone[RFC7231, Section 6.5.9]
+411   Length Required[RFC7231, Section 6.5.10]
+412   Precondition Failed[RFC7232, Section 4.2][RFC8144, Section 3.2]
+413   Payload Too Large[RFC7231, Section 6.5.11]
+414   URI Too Long[RFC7231, Section 6.5.12]
+415   Unsupported Media Type[RFC7231, Section 6.5.13][RFC7694, Section 3]
+416   Range Not Satisfiable[RFC7233, Section 4.4]
+417   Expectation Failed[RFC7231, Section 6.5.14]
+421   Misdirected Request[RFC7540, Section 9.1.2]
+422   Unprocessable Entity[RFC4918]
+423   Locked[RFC4918]
+424   Failed Dependency[RFC4918]
+425   Too Early[RFC8470]
+426   Upgrade Required[RFC7231, Section 6.5.15]
+427   Unassigned
+428   Precondition Required[RFC6585]
+429   Too Many Requests[RFC6585]
+430   Unassigned
+431   Request Header Fields Too Large[RFC6585]
+451   Unavailable For Legal Reasons[RFC7725]
+500   Internal Server Error[RFC7231, Section 6.6.1]
+501   Not Implemented[RFC7231, Section 6.6.2]
+502   Bad Gateway[RFC7231, Section 6.6.3]
+503   Service Unavailable[RFC7231, Section 6.6.4]
+504   Gateway Timeout[RFC7231, Section 6.6.5]
+505   HTTP Version Not Supported[RFC7231, Section 6.6.6]
+506   Variant Also Negotiates[RFC2295]
+507   Insufficient Storage[RFC4918]
+508   Loop Detected[RFC5842]
+509   Unassigned
+510   Not Extended[RFC2774]
+511   Network Authentication Required[RFC6585]
+*/
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ServerMonitor/Objects/Checks/PingCheck.cs	Sun Jan 06 20:49:08 2019 -0500
@@ -0,0 +1,56 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Net.NetworkInformation;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace ServerMonitorApp
+{
+    [DisplayName("Ping check"), Description("Check if the server responds to a ping request"), DisplayWeight(0)]
+    public class PingCheck : Check
+    {
+        protected async override Task<CheckResult> ExecuteCheckAsync(CancellationToken token)
+        {
+            using (Ping ping = new Ping())
+            {
+                try
+                {
+                    PingReply reply = await ping.SendPingAsync(Server.Host, Timeout);
+                    if (reply.Status == IPStatus.Success)
+                        return Pass(string.Format("Reply received in {0} ms", reply.RoundtripTime));
+                    else
+                        return Fail("Ping result: " + reply.Status);
+                }
+                catch (PingException e)
+                {
+                    return Fail(e);
+                }
+            }
+
+            // Cancellable version that doesn't actulaly work
+            // might be useful as a TaskCompletionSource example though
+            //
+            //TaskCompletionSource<CheckResult> tcs = new TaskCompletionSource<CheckResult
+            //
+            //using (Ping ping = new Ping())
+            //{
+            //    token.Register(ping.SendAsyncCancel);
+            //    ping.PingCompleted += (sender, e) =>
+            //    {
+            //        if (e.Error != null)
+            //            tcs.SetResult(Fail("Ping failed: " + e.Error.GetBaseException().Message));
+            //        else if (e.Reply.Status != IPStatus.Success)
+            //            tcs.SetResult(Fail("Ping failed: " + e.Reply.Status.ToString()));
+            //        else
+            //            tcs.SetResult(Pass("Ping completed in " + e.Reply.RoundtripTime + "ms"));
+            //    };
+            //    ping.SendAsync(Server.Host, Timeout, null);
+            //}
+
+            //return tcs.Task;
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ServerMonitor/Objects/Checks/SshCheck.cs	Sun Jan 06 20:49:08 2019 -0500
@@ -0,0 +1,115 @@
+using Renci.SshNet;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace ServerMonitorApp
+{
+    [DisplayName("SSH check"), Description("Check the result of a command run over SSH"), DisplayWeight(10)]
+    public class SshCheck : Check
+    {
+        public string Command { get; set; }
+
+        public bool CheckExitCode { get; set; }
+
+        public int ExitCode { get; set; }
+
+        public bool CheckCommandOutput { get; set; }
+
+        public MatchType CommandOutputMatchType { get; set; }
+
+        public string CommandOutputPattern { get; set; }
+
+        public bool CommandOutputUseRegex { get; set; }
+
+        protected override Task<CheckResult> ExecuteCheckAsync(CancellationToken token)
+        {
+            return Task.Run(() =>
+            {
+                try
+                {
+                    if (!Server.SshClient.IsConnected)
+                        Server.SshClient.Connect();
+                    token.ThrowIfCancellationRequested();
+                    using (SshCommand command = Server.SshClient.CreateCommand(GetCommand()))
+                    {
+                        token.Register(command.CancelAsync);
+                        IAsyncResult ar = command.BeginExecute();
+                        token.ThrowIfCancellationRequested();
+                        string output = (command.EndExecute(ar).Trim() + command.Error.Trim()).ConvertNewlines();
+                        return MergeResults(ProcessCommandResult(output, command.ExitStatus).ToArray());
+                    }
+                }
+                catch (Exception e)
+                {
+                    return Fail(e);
+                }
+            }, token);
+            //TaskCompletionSource<CheckResult> tcs = new TaskCompletionSource<CheckResult>();
+
+            ////TODO timeout
+            //if (!Server.SshClient.IsConnected)
+            //    Server.SshClient.Connect();
+            //using (SshCommand command = Server.SshClient.CreateCommand(Command))
+            //{
+            //    token.Register(command.CancelAsync);
+            //    command.BeginExecute(asyncResult =>
+            //    {
+            //        string result = command.EndExecute(asyncResult);
+            //        tcs.SetResult(new CheckResult(this, CheckStatus.Success, result));
+            //    });
+            //}
+
+            //return tcs.Task;
+        }
+
+        protected virtual string GetCommand()
+        {
+            return Command;
+        }
+
+        protected virtual List<CheckResult> ProcessCommandResult(string output, int exitCode)
+        {
+            List<CheckResult> results = new List<CheckResult>();
+            if (CheckExitCode)
+                results.Add(GetIntResult(ExitCode, exitCode, "Exit code"));
+            if (CheckCommandOutput)
+                results.Add(GetStringResult(CommandOutputMatchType, CommandOutputPattern, CommandOutputUseRegex, output, "Command output"));
+            return results;
+        }
+
+        public override string Validate(bool saving = true)
+        {
+            string message = base.Validate();
+            if (Server.Port <= 0)
+                message += "Server SSH port is required." + Environment.NewLine;
+            if (Server.Username.IsNullOrEmpty())
+                message += "Server SSH username is required." + Environment.NewLine;
+            if (Server.LoginType == LoginType.Password && Server.Password.IsNullOrEmpty())
+                message += "Server SSH password is required." + Environment.NewLine;
+            if (Server.LoginType == LoginType.PrivateKey && Server.KeyFile.IsNullOrEmpty())
+                message += "Server SSH key is required." + Environment.NewLine;
+            if (Command.IsNullOrEmpty())
+                message += "Command is required." + Environment.NewLine;
+            if (!CheckExitCode && !CheckCommandOutput)
+                message += "At least one check must be enabled." + Environment.NewLine;
+            if (CheckCommandOutput && CommandOutputUseRegex)
+            {
+                try
+                {
+                    Regex re = new Regex(CommandOutputPattern);
+                }
+                catch (ArgumentException)
+                {
+                    message += "Invalid regular expression for command output." + Environment.NewLine;
+                }
+            }
+            return message;
+        }
+    }
+}
--- a/ServerMonitor/Objects/HttpCheck.cs	Tue Jan 01 21:14:47 2019 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,135 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Linq;
-using System.Text;
-using System.Text.RegularExpressions;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace ServerMonitorApp
-{
-    [DisplayName("HTTP check"), Description("Check the result of an HTTP request"), DisplayWeight(1)]
-    public class HttpCheck : Check
-    {
-        public string Url { get; set; }
-
-        public bool CheckResponseCode { get; set; }
-
-        public int ResponseCode { get; set; }
-
-        public bool CheckResponseLength { get; set; }
-
-        public string ResponseLengthMin { get; set; }
-
-        public string ResponseLengthMax { get; set; }
-
-        public bool CheckResponseBody { get; set; }
-
-        public MatchType ResponseBodyMatchType { get; set; }
-
-        public string ResponseBodyPattern { get; set; }
-
-        public bool ResponseBodyUseRegex { get; set; }
-
-        protected override Task<CheckResult> ExecuteCheckAsync(CancellationToken token)
-        {
-            throw new NotImplementedException();
-        }
-
-        public override string Validate(bool saving = true)
-        {
-            string message = base.Validate();
-            if (Url.IsNullOrEmpty())
-                message += "URL cannot be blank." + Environment.NewLine;
-            if (!CheckResponseCode && !CheckResponseLength && !CheckResponseBody)
-                message += "At least one check must be enabled." + Environment.NewLine;
-            if (CheckResponseBody && ResponseBodyUseRegex)
-            {
-                try
-                {
-                    Regex re = new Regex(ResponseBodyPattern);
-                }
-                catch (ArgumentException)
-                {
-                    message += "Invalid regular expression for response body." + Environment.NewLine;
-                }
-            }
-            return message;
-        }
-
-        //protected override CheckResult GetIntResult(int expectedValue, int resultValue, string description)
-        //{
-        //    CheckResult result = base.GetIntResult(expectedValue, resultValue, description);
-
-        //}
-
-/*
-100   Continue[RFC7231, Section 6.2.1]
-101   Switching Protocols[RFC7231, Section 6.2.2]
-102   Processing[RFC2518]
-103   Early Hints[RFC8297]
-200   OK[RFC7231, Section 6.3.1]
-201   Created[RFC7231, Section 6.3.2]
-202   Accepted[RFC7231, Section 6.3.3]
-203   Non-Authoritative Information[RFC7231, Section 6.3.4]
-204   No Content[RFC7231, Section 6.3.5]
-205   Reset Content[RFC7231, Section 6.3.6]
-206   Partial Content[RFC7233, Section 4.1]
-207   Multi-Status[RFC4918]
-208   Already Reported[RFC5842]
-226   IM Used[RFC3229]
-300   Multiple Choices[RFC7231, Section 6.4.1]
-301   Moved Permanently[RFC7231, Section 6.4.2]
-302   Found[RFC7231, Section 6.4.3]
-303   See Other[RFC7231, Section 6.4.4]
-304   Not Modified[RFC7232, Section 4.1]
-305   Use Proxy[RFC7231, Section 6.4.5]
-306   (Unused)[RFC7231, Section 6.4.6]
-307   Temporary Redirect[RFC7231, Section 6.4.7]
-308   Permanent Redirect[RFC7538]
-400   Bad Request[RFC7231, Section 6.5.1]
-401   Unauthorized[RFC7235, Section 3.1]
-402   Payment Required[RFC7231, Section 6.5.2]
-403   Forbidden[RFC7231, Section 6.5.3]
-404   Not Found[RFC7231, Section 6.5.4]
-405   Method Not Allowed[RFC7231, Section 6.5.5]
-406   Not Acceptable[RFC7231, Section 6.5.6]
-407   Proxy Authentication Required[RFC7235, Section 3.2]
-408   Request Timeout[RFC7231, Section 6.5.7]
-409   Conflict[RFC7231, Section 6.5.8]
-410   Gone[RFC7231, Section 6.5.9]
-411   Length Required[RFC7231, Section 6.5.10]
-412   Precondition Failed[RFC7232, Section 4.2][RFC8144, Section 3.2]
-413   Payload Too Large[RFC7231, Section 6.5.11]
-414   URI Too Long[RFC7231, Section 6.5.12]
-415   Unsupported Media Type[RFC7231, Section 6.5.13][RFC7694, Section 3]
-416   Range Not Satisfiable[RFC7233, Section 4.4]
-417   Expectation Failed[RFC7231, Section 6.5.14]
-421   Misdirected Request[RFC7540, Section 9.1.2]
-422   Unprocessable Entity[RFC4918]
-423   Locked[RFC4918]
-424   Failed Dependency[RFC4918]
-425   Too Early[RFC8470]
-426   Upgrade Required[RFC7231, Section 6.5.15]
-427   Unassigned
-428   Precondition Required[RFC6585]
-429   Too Many Requests[RFC6585]
-430   Unassigned
-431   Request Header Fields Too Large[RFC6585]
-451   Unavailable For Legal Reasons[RFC7725]
-500   Internal Server Error[RFC7231, Section 6.6.1]
-501   Not Implemented[RFC7231, Section 6.6.2]
-502   Bad Gateway[RFC7231, Section 6.6.3]
-503   Service Unavailable[RFC7231, Section 6.6.4]
-504   Gateway Timeout[RFC7231, Section 6.6.5]
-505   HTTP Version Not Supported[RFC7231, Section 6.6.6]
-506   Variant Also Negotiates[RFC2295]
-507   Insufficient Storage[RFC4918]
-508   Loop Detected[RFC5842]
-509   Unassigned
-510   Not Extended[RFC2774]
-511   Network Authentication Required[RFC6585]
-*/
-    }
-}
--- a/ServerMonitor/Objects/PingCheck.cs	Tue Jan 01 21:14:47 2019 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,56 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Linq;
-using System.Net.NetworkInformation;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace ServerMonitorApp
-{
-    [DisplayName("Ping check"), Description("Check if the server responds to a ping request"), DisplayWeight(0)]
-    public class PingCheck : Check
-    {
-        protected async override Task<CheckResult> ExecuteCheckAsync(CancellationToken token)
-        {
-            using (Ping ping = new Ping())
-            {
-                try
-                {
-                    PingReply reply = await ping.SendPingAsync(Server.Host, Timeout);
-                    if (reply.Status == IPStatus.Success)
-                        return Pass(string.Format("Reply received in {0} ms", reply.RoundtripTime));
-                    else
-                        return Fail("Ping result: " + reply.Status);
-                }
-                catch (PingException e)
-                {
-                    return Fail(e);
-                }
-            }
-
-            // Cancellable version that doesn't actulaly work
-            // might be useful as a TaskCompletionSource example though
-            //
-            //TaskCompletionSource<CheckResult> tcs = new TaskCompletionSource<CheckResult
-            //
-            //using (Ping ping = new Ping())
-            //{
-            //    token.Register(ping.SendAsyncCancel);
-            //    ping.PingCompleted += (sender, e) =>
-            //    {
-            //        if (e.Error != null)
-            //            tcs.SetResult(Fail("Ping failed: " + e.Error.GetBaseException().Message));
-            //        else if (e.Reply.Status != IPStatus.Success)
-            //            tcs.SetResult(Fail("Ping failed: " + e.Reply.Status.ToString()));
-            //        else
-            //            tcs.SetResult(Pass("Ping completed in " + e.Reply.RoundtripTime + "ms"));
-            //    };
-            //    ping.SendAsync(Server.Host, Timeout, null);
-            //}
-
-            //return tcs.Task;
-        }
-    }
-}
--- a/ServerMonitor/Objects/SshCheck.cs	Tue Jan 01 21:14:47 2019 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,106 +0,0 @@
-using Renci.SshNet;
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Linq;
-using System.Text;
-using System.Text.RegularExpressions;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace ServerMonitorApp
-{
-    [DisplayName("SSH check"), Description("Check the result of a command run over SSH"), DisplayWeight(10)]
-    public class SshCheck : Check
-    {
-        public string Command { get; set; }
-
-        public bool CheckExitCode { get; set; }
-
-        public int ExitCode { get; set; }
-
-        public bool CheckCommandOutput { get; set; }
-
-        public MatchType CommandOutputMatchType { get; set; }
-
-        public string CommandOutputPattern { get; set; }
-
-        public bool CommandOutputUseRegex { get; set; }
-
-        protected override Task<CheckResult> ExecuteCheckAsync(CancellationToken token)
-        {
-            return Task.Run(() =>
-            {
-                try
-                {
-                    if (!Server.SshClient.IsConnected)
-                        Server.SshClient.Connect();
-                    token.ThrowIfCancellationRequested();
-                    using (SshCommand command = Server.SshClient.CreateCommand(Command))
-                    {
-                        token.Register(command.CancelAsync);
-                        IAsyncResult ar = command.BeginExecute();
-                        token.ThrowIfCancellationRequested();
-                        string result = command.EndExecute(ar).Trim() + command.Error.Trim();
-
-                        CheckResult exitCodeResult = null, commandOutputResult = null;
-                        if (CheckExitCode)
-                            exitCodeResult = GetIntResult(ExitCode, command.ExitStatus, "Exit code");
-                        if (CheckCommandOutput)
-                            commandOutputResult = GetStringResult(CommandOutputMatchType, CommandOutputPattern, CommandOutputUseRegex, result, "Command output");
-                        return MergeResults(exitCodeResult, commandOutputResult);
-                    }
-                }
-                catch (Exception e)
-                {
-                    return Fail(e);
-                }
-            }, token);
-            //TaskCompletionSource<CheckResult> tcs = new TaskCompletionSource<CheckResult>();
-
-            ////TODO timeout
-            //if (!Server.SshClient.IsConnected)
-            //    Server.SshClient.Connect();
-            //using (SshCommand command = Server.SshClient.CreateCommand(Command))
-            //{
-            //    token.Register(command.CancelAsync);
-            //    command.BeginExecute(asyncResult =>
-            //    {
-            //        string result = command.EndExecute(asyncResult);
-            //        tcs.SetResult(new CheckResult(this, CheckStatus.Success, result));
-            //    });
-            //}
-
-            //return tcs.Task;
-        }
-
-        public override string Validate(bool saving = true)
-        {
-            string message = base.Validate();
-            if (Server.Port <= 0)
-                message += "Server SSH port is required." + Environment.NewLine;
-            if (Server.Username.IsNullOrEmpty())
-                message += "Server SSH username is required." + Environment.NewLine;
-            if (Server.LoginType == LoginType.Password && Server.Password.IsNullOrEmpty())
-                message += "Server SSH password is required." + Environment.NewLine;
-            if (Server.LoginType == LoginType.PrivateKey && Server.KeyFile.IsNullOrEmpty())
-                message += "Server SSH key is required." + Environment.NewLine;
-            if (Command.IsNullOrEmpty())
-                message += "Command is required." + Environment.NewLine;
-            if (!CheckExitCode && !CheckCommandOutput)
-                message += "At least one check must be enabled." + Environment.NewLine;
-            if (CheckCommandOutput && CommandOutputUseRegex)
-            {
-                try
-                {
-                    Regex re = new Regex(CommandOutputPattern);
-                }
-                catch (ArgumentException)
-                {
-                    message += "Invalid regular expression for command output." + Environment.NewLine;
-                }
-            }
-            return message;
-        }
-    }
-}
--- a/ServerMonitor/ServerMonitor.csproj	Tue Jan 01 21:14:47 2019 -0500
+++ b/ServerMonitor/ServerMonitor.csproj	Sun Jan 06 20:49:08 2019 -0500
@@ -62,6 +62,12 @@
     <Compile Include="Controls\CheckControl.Designer.cs">
       <DependentUpon>CheckControl.cs</DependentUpon>
     </Compile>
+    <Compile Include="Controls\DiskSpaceCheckControl.cs">
+      <SubType>UserControl</SubType>
+    </Compile>
+    <Compile Include="Controls\DiskSpaceCheckControl.Designer.cs">
+      <DependentUpon>DiskSpaceCheckControl.cs</DependentUpon>
+    </Compile>
     <Compile Include="Controls\SshCheckControl.cs">
       <SubType>UserControl</SubType>
     </Compile>
@@ -77,6 +83,7 @@
     <Compile Include="Controls\MatchComboBox.cs">
       <SubType>Component</SubType>
     </Compile>
+    <Compile Include="Objects\Checks\DiskSpaceCheck.cs" />
     <Compile Include="Objects\UpdateCheckException.cs" />
     <Compile Include="Forms\CheckBoxDialog.cs">
       <SubType>Form</SubType>
@@ -97,11 +104,11 @@
       <DependentUpon>QuickHelpForm.cs</DependentUpon>
     </Compile>
     <Compile Include="Helpers.cs" />
-    <Compile Include="Objects\Check.cs" />
+    <Compile Include="Objects\Checks\Check.cs" />
     <Compile Include="Objects\CheckResult.cs" />
-    <Compile Include="Objects\HttpCheck.cs" />
+    <Compile Include="Objects\Checks\HttpCheck.cs" />
     <Compile Include="Objects\Logger.cs" />
-    <Compile Include="Objects\PingCheck.cs" />
+    <Compile Include="Objects\Checks\PingCheck.cs" />
     <Compile Include="Objects\Schedule.cs" />
     <Compile Include="Objects\Server.cs" />
     <Compile Include="Forms\ServerForm.cs">
@@ -117,7 +124,7 @@
     <Compile Include="Forms\ServerSummaryForm.Designer.cs">
       <DependentUpon>ServerSummaryForm.cs</DependentUpon>
     </Compile>
-    <Compile Include="Objects\SshCheck.cs" />
+    <Compile Include="Objects\Checks\SshCheck.cs" />
     <Compile Include="Program.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="Controls\ServerSummaryControl.cs">
@@ -129,6 +136,9 @@
     <EmbeddedResource Include="Controls\CheckControl.resx">
       <DependentUpon>CheckControl.cs</DependentUpon>
     </EmbeddedResource>
+    <EmbeddedResource Include="Controls\DiskSpaceCheckControl.resx">
+      <DependentUpon>DiskSpaceCheckControl.cs</DependentUpon>
+    </EmbeddedResource>
     <EmbeddedResource Include="Controls\SshCheckControl.resx">
       <DependentUpon>SshCheckControl.cs</DependentUpon>
     </EmbeddedResource>
@@ -217,5 +227,6 @@
   <ItemGroup>
     <None Include="Resources\pass.png" />
   </ItemGroup>
+  <ItemGroup />
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
 </Project>
\ No newline at end of file