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
{
/// The possible statuses of a check.
///
/// The integer values of the "completed" statuses (Success, Information,
/// Warning, Error) are in ascending order of severity.
///
public enum CheckStatus
{
Success = 0,
Information = 1,
Warning = 2,
Error = 3,
Running = 4,
Disabled = 5,
}
/// Base class for checks that run against a server and return a result status.
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()?.DisplayWeight).ToArray());
}
}
/// The internal ID of the check.
public int Id { get; set; }
/// The display name check.
public string Name { get; set; }
/// The number of milliseconds the check must complete in before reporting failure.
public int Timeout { get; set; }
/// Whether the check will be executed on a schedule.
public bool Enabled { get; set; }
/// The schedule when the check will be executed.
public Schedule Schedule { get; set; }
/// The date and time the check was last executed.
public DateTime LastRunTime { get; set; }
/// The date and time the check was last executed on its schedule.
public DateTime LastScheduledRunTime { get; set; }
/// The date and time the check is currently scheduled to execute next.
public DateTime NextRunTime { get; set; }
/// The text output of the last execution of the check.
public string LastMessage { get; set; }
/// The current status of the check.
public CheckStatus Status { get; set; }
/// The status of the last check execution.
public CheckStatus LastRunStatus { get; set; }
/// The severity level reported when the check execution fails.
public CheckStatus FailStatus { get; set; }
/// The number of consecutive failed executions before the check begins reporting failure.
public int MaxConsecutiveFailures { get; set; }
/// The current number of consecutive times the check has failed.
[XmlIgnore]
public int ConsecutiveFailures { get; set; }
/// The server the check belongs to.
[XmlIgnore]
public Server Server { get; set; }
/// Check constructor.
public Check()
{
// Set the default failure severity to Error.
FailStatus = CheckStatus.Error;
}
/// Displays the check name.
public override string ToString()
{
return Name;
}
/// Validates the check.
///
/// Whether the check is being saved.
/// Checks are validated before being saved and before being executed. This parameter allows
/// them to distinguish between these cases.
///
/// An empty string if the check is valid, or the reason the check is invalid.
public virtual string Validate(bool saving = true)
{
string message = string.Empty;
// Allow blank names if the check is being executed before saving.
// This lets the user create a check and tinker with it without
// needing to type a name unless they want to save it.
if (Name.IsNullOrEmpty() && saving)
message += "Name cannot be blank." + Environment.NewLine;
return message;
}
/// Executes the check asynchronously.
/// A token for cancelling the execution.
/// Whether the check status and last execution time should be updated when the check completes.
/// The result of the check execution.
public async Task ExecuteAsync(CancellationToken token = default(CancellationToken), bool update = true)
{
// Do nothing if the check has already been cancelled.
if (token.IsCancellationRequested)
return null;
CheckResult result;
DateTime startTime = DateTime.Now;
try
{
// Execute the check.
Task checkTask = ExecuteCheckAsync(token);
try
{
// Wait for the check to complete or timeout, whichever happens first.
if (await Task.WhenAny(checkTask, Task.Delay(Timeout, token)) == checkTask)
{
// If the check completed before timing out, retrieve the result.
result = await checkTask;
}
else
{
// If the check timed out before completing, report failure.
result = Fail("Timed out.");
}
}
catch (TaskCanceledException)
{
// If the check was cancelled, do not return a result so it will not be logged.
return null;
}
}
catch (Exception e)
{
// If the execution threw an exception, report the exception as a failure.
result = Fail(e.GetBaseException().Message);
}
result.StartTime = startTime;
result.EndTime = DateTime.Now;
if (update)
{
Status = result.CheckStatus;
LastRunStatus = result.CheckStatus;
LastMessage = result.Message;
LastRunTime = result.EndTime;
}
return result;
}
/// Generates a successful check result.
/// The execution result message.
/// A successful check result.
public CheckResult Pass(string message)
{
return new CheckResult(this, CheckStatus.Success, message);
}
/// Generates a failed check result.
/// The execution result message.
/// A failed check result.
public CheckResult Fail(string message)
{
// The severity is controlled by the check's FailStatus setting.
return new CheckResult(this, FailStatus, message);
}
/// Generates a failed check result from an exception.
/// The exception that caused the failure.
/// A failed check result.
protected CheckResult Fail(Exception e)
{
return Fail(e.GetBaseException().Message);
}
/// Generates a check result by comparing integer values for equality.
/// The expected result value.
/// The actual result value generated by the check execution.
/// Description of what the integer represents to use in the check result message. Example: "Exit code".
/// A successful check result if the values are equal, or a failed check result if they are unequal.
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));
}
/// Generates a check result by comparing string values.
/// The comparison that will be used on the strings.
/// The expected pattern to test the result against.
/// Whether the expected pattern should be treated as a regular expression.
/// The actual result value generated by the check execution.
/// Description of what the string represents to use in the check result message.
/// A successful check result if the string comparison succeeds, or a failed check result if it fails.
protected CheckResult GetStringResult(MatchType matchType, string expectedPattern, bool useRegex, string resultValue, string description)
{
bool match;
if (useRegex)
{
// If the match type is equals or not equals, modify the regex by
// adding beginning and ending anchors if not already present
// to prevent partial matches.
if (matchType.In(MatchType.Equals, MatchType.NotEquals))
{
if (!expectedPattern.StartsWith("^"))
expectedPattern = "^" + expectedPattern;
if (!expectedPattern.EndsWith("$"))
expectedPattern += "$";
}
// Execute the regex.
Regex re = new Regex(expectedPattern, RegexOptions.Singleline);
match = re.IsMatch(resultValue);
}
else
{
// Simple string comparisons.
if (matchType.In(MatchType.Equals, MatchType.NotEquals))
{
match = expectedPattern == resultValue;
}
else if (matchType.In(MatchType.Contains, MatchType.NotContains))
{
match = resultValue.Contains(expectedPattern);
}
else
{
// If the match type is greater or less than, the values must be numeric.
if (decimal.TryParse(expectedPattern, out decimal expectedNumeric) &&
decimal.TryParse(resultValue, out decimal resultNumeric))
{
// Compare the resulting decimals.
match = (matchType == MatchType.GreaterThan && resultNumeric > expectedNumeric) ||
(matchType == MatchType.LessThan && resultNumeric < expectedNumeric);
}
else
{
return Fail(string.Format("{0} is not numeric: {1}", description, resultValue));
}
}
}
// We have determined whether the result value matches the expected pattern.
// Generate a check result accordingly.
if (matchType.In(MatchType.Equals, MatchType.Contains))
{
// Equals, Contains: the strings are supposed to match.
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))
{
// NotEquals, NotContains: the strings are not supposed to match.
// So, fail if they do match and pass if they do not.
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
{
// GreaterThan, LessThan
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));
}
}
/// Merges multiple execution results.
/// The results to merge.
/// A single result containing the messages of all the input results, and a failure status if any of the input results failed.
///
/// Some checks may want to run several tests on the result of a remote command.
/// After collecting their results, they can use this method to combine them into
/// a single result that will be reported to the user.
///
protected CheckResult MergeResults(params CheckResult[] results)
{
StringBuilder message = new StringBuilder();
bool failed = false;
foreach (CheckResult result in results)
{
if (result == null)
continue;
// Report failure if any of the results has failed.
if (result.Failed)
failed = true;
// Merge the result messages.
message.AppendLine(result.Message);
}
return failed ? Fail(message.ToString().Trim()) : Pass(message.ToString().Trim());
}
/// Executes the check asynchronously.
/// A token for cancelling the execution.
/// The result of the check execution.
protected abstract Task ExecuteCheckAsync(CancellationToken token);
}
}