comparison ServerMonitor/Objects/Checks/Check.cs @ 16:7626b099aefd

More comments.
author Brad Greco <brad@bgreco.net>
date Tue, 30 Apr 2019 20:40:58 -0400
parents 052aa62cb42a
children 7645122aa7a9
comparison
equal deleted inserted replaced
15:23f2e0da1094 16:7626b099aefd
6 using System.Threading.Tasks; 6 using System.Threading.Tasks;
7 using System.Xml.Serialization; 7 using System.Xml.Serialization;
8 8
9 namespace ServerMonitorApp 9 namespace ServerMonitorApp
10 { 10 {
11 /*public enum CheckType 11 /// <summary>The possible statuses of a check.</summary>
12 { 12 /// <remarks>
13 Command 13 /// The integer values of the "completed" statuses (Success, Information,
14 }*/ 14 /// Warning, Error) are in ascending order of severity.
15 15 /// </remarks>
16 public enum CheckStatus 16 public enum CheckStatus
17 { 17 {
18 Success, 18 Success,
19 Information, 19 Information,
20 Warning, 20 Warning,
21 Error, 21 Error,
22 Running, 22 Running,
23 Disabled, 23 Disabled,
24 } 24 }
25 25
26 /// <summary>Base class for checks that run against a server and return a result status.</summary>
26 public abstract class Check 27 public abstract class Check
27 { 28 {
28 private static Type[] _checkTypes; 29 private static Type[] _checkTypes;
29 30
30 public static Type[] CheckTypes 31 public static Type[] CheckTypes
35 .Where(t => t.IsSubclassOf(typeof(Check))) 36 .Where(t => t.IsSubclassOf(typeof(Check)))
36 .OrderBy(t => t.GetAttribute<DisplayWeightAttribute>()?.DisplayWeight).ToArray()); 37 .OrderBy(t => t.GetAttribute<DisplayWeightAttribute>()?.DisplayWeight).ToArray());
37 } 38 }
38 } 39 }
39 40
41 /// <summary>The internal ID of the check.</summary>
40 public int Id { get; set; } 42 public int Id { get; set; }
41 43
44 /// <summary>The display name check.</summary>
42 public string Name { get; set; } 45 public string Name { get; set; }
43 46
44 /*public CheckType Type { get; set; }*/ 47 /// <summary>The number of milliseconds the check must complete in before reporting failure.</summary>
45
46 public int Timeout { get; set; } 48 public int Timeout { get; set; }
47 49
50 /// <summary>Whether the check will be executed on a schedule.</summary>
48 public bool Enabled { get; set; } 51 public bool Enabled { get; set; }
49 52
53 /// <summary>The schedule when the check will be executed.</summary>
50 public Schedule Schedule { get; set; } 54 public Schedule Schedule { get; set; }
51 55
56 /// <summary>The date and time the check was last executed.</summary>
52 public DateTime LastRunTime { get; set; } 57 public DateTime LastRunTime { get; set; }
53 58
59 /// <summary>The date and time the check was last executed on its schedule.</summary>
54 public DateTime LastScheduledRunTime { get; set; } 60 public DateTime LastScheduledRunTime { get; set; }
55 61
62 /// <summary>The date and time the check is currently scheduled to execute next.</summary>
56 public DateTime NextRunTime { get; set; } 63 public DateTime NextRunTime { get; set; }
57 64
65 /// <summary>The text output of the last execution of the check.</summary>
58 public string LastMessage { get; set; } 66 public string LastMessage { get; set; }
59 67
68 /// <summary>The current status of the check.</summary>
60 public CheckStatus Status { get; set; } 69 public CheckStatus Status { get; set; }
61 70
71 /// <summary>The status of the last check execution.</summary>
62 public CheckStatus LastRunStatus { get; set; } 72 public CheckStatus LastRunStatus { get; set; }
63 73
74 /// <summary>The severity level reported when the check execution fails.</summary>
64 public CheckStatus FailStatus { get; set; } 75 public CheckStatus FailStatus { get; set; }
65 76
77 /// <summary>The number of consecutive failed executions before the check begins reporting failure.</summary>
66 public int MaxConsecutiveFailures { get; set; } 78 public int MaxConsecutiveFailures { get; set; }
67 79
80 /// <summary>The current number of consecutive times the check has failed.</summary>
68 [XmlIgnore] 81 [XmlIgnore]
69 public int ConsecutiveFailures { get; set; } 82 public int ConsecutiveFailures { get; set; }
70 83
84 /// <summary>The server the check belongs to.</summary>
71 [XmlIgnore] 85 [XmlIgnore]
72 public Server Server { get; set; } 86 public Server Server { get; set; }
73 87
88 /// <summary>Check constructor.</summary>
74 public Check() 89 public Check()
75 { 90 {
91 // Set the default failure severity to Error.
76 FailStatus = CheckStatus.Error; 92 FailStatus = CheckStatus.Error;
77 } 93 }
78 94
95 /// <summary>Displays the check name.</summary>
79 public override string ToString() 96 public override string ToString()
80 { 97 {
81 return Name; 98 return Name;
82 } 99 }
83 100
101 /// <summary>Validates the check.</summary>
102 /// <param name="saving">
103 /// Whether the check is being saved.
104 /// Checks are validated before being saved and before being executed. This parameter allows
105 /// them to distinguish between these cases.
106 /// </param>
107 /// <returns>An empty string if the check is valid, or the reason the check is invalid.</returns>
84 public virtual string Validate(bool saving = true) 108 public virtual string Validate(bool saving = true)
85 { 109 {
86 string message = string.Empty; 110 string message = string.Empty;
111 // Allow blank names if the check is being executed before saving.
112 // This lets the user create a check and tinker with it without
113 // needing to type a name unless they want to save it.
87 if (Name.IsNullOrEmpty() && saving) 114 if (Name.IsNullOrEmpty() && saving)
88 message += "Name cannot be blank." + Environment.NewLine; 115 message += "Name cannot be blank." + Environment.NewLine;
89 return message; 116 return message;
90 } 117 }
91 118
119 /// <summary>Executes the check asynchronously.</summary>
120 /// <param name="token">A token for cancelling the execution.</param>
121 /// <param name="update">Whether the check status and last execution time should be updated when the check completes.</param>
122 /// <returns>The result of the check execution.</returns>
92 public async Task<CheckResult> ExecuteAsync(CancellationToken token = default(CancellationToken), bool update = true) 123 public async Task<CheckResult> ExecuteAsync(CancellationToken token = default(CancellationToken), bool update = true)
93 { 124 {
125 // Do nothing if the check has already been cancelled.
94 if (token.IsCancellationRequested) 126 if (token.IsCancellationRequested)
95 return null; 127 return null;
96 128
97 CheckResult result; 129 CheckResult result;
98 DateTime startTime = DateTime.Now; 130 DateTime startTime = DateTime.Now;
99 try 131 try
100 { 132 {
133 // Execute the check.
101 Task<CheckResult> checkTask = ExecuteCheckAsync(token); 134 Task<CheckResult> checkTask = ExecuteCheckAsync(token);
102 try 135 try
103 { 136 {
137 // Wait for the check to complete or timeout, whichever happens first.
104 if (await Task.WhenAny(checkTask, Task.Delay(Timeout, token)) == checkTask) 138 if (await Task.WhenAny(checkTask, Task.Delay(Timeout, token)) == checkTask)
105 { 139 {
140 // If the check completed before timing out, retrieve the result.
106 result = await checkTask; 141 result = await checkTask;
107 } 142 }
108 else 143 else
109 { 144 {
145 // If the check timed out before completing, report failure.
110 result = Fail("Timed out."); 146 result = Fail("Timed out.");
111 } 147 }
112 } 148 }
113 catch (TaskCanceledException) 149 catch (TaskCanceledException)
114 { 150 {
151 // If the check was cancelled, do not return a result so it will not be logged.
115 return null; 152 return null;
116 } 153 }
117 } 154 }
118 catch (Exception e) 155 catch (Exception e)
119 { 156 {
157 // If the execution threw an exception, report the exception as a failure.
120 result = Fail(e.GetBaseException().Message); 158 result = Fail(e.GetBaseException().Message);
121 } 159 }
122 result.StartTime = startTime; 160 result.StartTime = startTime;
123 result.EndTime = DateTime.Now; 161 result.EndTime = DateTime.Now;
124 // If a check is executed from the CheckForm, we don't want to update the status or log the event.
125 if (update) 162 if (update)
126 { 163 {
127 Status = result.CheckStatus; 164 Status = result.CheckStatus;
128 LastRunStatus = result.CheckStatus; 165 LastRunStatus = result.CheckStatus;
129 LastMessage = result.Message; 166 LastMessage = result.Message;
130 LastRunTime = result.EndTime; 167 LastRunTime = result.EndTime;
131 } 168 }
132 return result; 169 return result;
133 } 170 }
134 171
172 /// <summary>Generates a successful check result.</summary>
173 /// <param name="message">The execution result message.</param>
174 /// <returns>A successful check result.</returns>
135 public CheckResult Pass(string message) 175 public CheckResult Pass(string message)
136 { 176 {
137 return new CheckResult(this, CheckStatus.Success, message); 177 return new CheckResult(this, CheckStatus.Success, message);
138 } 178 }
139 179
180 /// <summary>Generates a failed check result.</summary>
181 /// <param name="message">The execution result message.</param>
182 /// <returns>A failed check result.</returns>
140 public CheckResult Fail(string message) 183 public CheckResult Fail(string message)
141 { 184 {
185 // The severity is controlled by the check's FailStatus setting.
142 return new CheckResult(this, FailStatus, message); 186 return new CheckResult(this, FailStatus, message);
143 } 187 }
144 188
189 /// <summary>Generates a failed check result from an exception.</summary>
190 /// <param name="e">The exception that caused the failure.</param>
191 /// <returns>A failed check result.</returns>
145 protected CheckResult Fail(Exception e) 192 protected CheckResult Fail(Exception e)
146 { 193 {
147 return new CheckResult(this, FailStatus, e.GetBaseException().Message); 194 return Fail(e.GetBaseException().Message);
148 } 195 }
149 196
197 /// <summary>Generates a check result by comparing integer values for equality.</summary>
198 /// <param name="expectedValue">The expected result value.</param>
199 /// <param name="resultValue">The actual result value generated by the check execution.</param>
200 /// <param name="description">Description of what the integer represents to use in the check result message. Example: "Exit code".</param>
201 /// <returns>A successful check result if the values are equal, or a failed check result if they are unequal.</returns>
150 protected CheckResult GetIntResult(int expectedValue, int resultValue, string description) 202 protected CheckResult GetIntResult(int expectedValue, int resultValue, string description)
151 { 203 {
152 if (expectedValue == resultValue) 204 if (expectedValue == resultValue)
153 return Pass(string.Format("{0}: {1}", description, resultValue)); 205 return Pass(string.Format("{0}: {1}", description, resultValue));
154 else 206 else
155 return Fail(string.Format("{0}: {1} (expected: {2})", description, resultValue, expectedValue)); 207 return Fail(string.Format("{0}: {1} (expected: {2})", description, resultValue, expectedValue));
156 } 208 }
157 209
210 /// <summary>Generates a check result by comparing string values.</summary>
211 /// <param name="matchType">The comparison that will be used on the strings.</param>
212 /// <param name="expectedPattern">The expected pattern to test the result against.</param>
213 /// <param name="useRegex">Whether the expected pattern should be treated as a regular expression.</param>
214 /// <param name="resultValue">The actual result value generated by the check execution.</param>
215 /// <param name="description">Description of what the string represents to use in the check result message.</param>
216 /// <returns>A successful check result if the string comparison succeeds, or a failed check result if it fails.</returns>
158 protected CheckResult GetStringResult(MatchType matchType, string expectedPattern, bool useRegex, string resultValue, string description) 217 protected CheckResult GetStringResult(MatchType matchType, string expectedPattern, bool useRegex, string resultValue, string description)
159 { 218 {
160 bool match; 219 bool match;
161 if (useRegex) 220 if (useRegex)
162 { 221 {
222 // If the match type is equals or not equals, modify the regex by
223 // adding beginning and ending anchors if not already present
224 // to prevent partial matches.
163 if (matchType.In(MatchType.Equals, MatchType.NotEquals)) 225 if (matchType.In(MatchType.Equals, MatchType.NotEquals))
164 { 226 {
165 if (!expectedPattern.StartsWith("^")) 227 if (!expectedPattern.StartsWith("^"))
166 expectedPattern = "^" + expectedPattern; 228 expectedPattern = "^" + expectedPattern;
167 if (!expectedPattern.EndsWith("$")) 229 if (!expectedPattern.EndsWith("$"))
168 expectedPattern += "$"; 230 expectedPattern += "$";
169 } 231 }
232 // Execute the regex.
170 Regex re = new Regex(expectedPattern, RegexOptions.Singleline); 233 Regex re = new Regex(expectedPattern, RegexOptions.Singleline);
171 match = re.IsMatch(resultValue); 234 match = re.IsMatch(resultValue);
172 } 235 }
173 else 236 else
174 { 237 {
238 // Simple string comparisons.
175 if (matchType.In(MatchType.Equals, MatchType.NotEquals)) 239 if (matchType.In(MatchType.Equals, MatchType.NotEquals))
176 { 240 {
177 match = expectedPattern == resultValue; 241 match = expectedPattern == resultValue;
178 } 242 }
179 else if (matchType.In(MatchType.Contains, MatchType.NotContains)) 243 else if (matchType.In(MatchType.Contains, MatchType.NotContains))
180 { 244 {
181 match = resultValue.Contains(expectedPattern); 245 match = resultValue.Contains(expectedPattern);
182 } 246 }
183 else 247 else
184 { 248 {
249 // If the match type is greater or less than, the values must be numeric.
185 if (decimal.TryParse(expectedPattern, out decimal expectedNumeric) && 250 if (decimal.TryParse(expectedPattern, out decimal expectedNumeric) &&
186 decimal.TryParse(resultValue, out decimal resultNumeric)) 251 decimal.TryParse(resultValue, out decimal resultNumeric))
187 { 252 {
253 // Compare the resulting decimals.
188 match = (matchType == MatchType.GreaterThan && resultNumeric > expectedNumeric) || 254 match = (matchType == MatchType.GreaterThan && resultNumeric > expectedNumeric) ||
189 (matchType == MatchType.LessThan && resultNumeric < expectedNumeric); 255 (matchType == MatchType.LessThan && resultNumeric < expectedNumeric);
190 } 256 }
191 else 257 else
192 { 258 {
193 return Fail(string.Format("{0} is not numeric: {1}", description, resultValue)); 259 return Fail(string.Format("{0} is not numeric: {1}", description, resultValue));
194 } 260 }
195 } 261 }
196 } 262 }
197 263
264 // We have determined whether the result value matches the expected pattern.
265 // Generate a check result accordingly.
198 if (matchType.In(MatchType.Equals, MatchType.Contains)) 266 if (matchType.In(MatchType.Equals, MatchType.Contains))
199 { 267 {
268 // Equals, Contains: the strings are supposed to match.
200 if (match) 269 if (match)
201 return Pass(string.Format("{0} {1} the pattern: {2}", description, matchType.ToString().ToLower(), expectedPattern)); 270 return Pass(string.Format("{0} {1} the pattern: {2}", description, matchType.ToString().ToLower(), expectedPattern));
202 else 271 else
203 return Fail(string.Format("{0} does not {1} the pattern: {2} ({0}: {3})", description, matchType.ToString().ToLower().TrimEnd('s'), expectedPattern, resultValue)); 272 return Fail(string.Format("{0} does not {1} the pattern: {2} ({0}: {3})", description, matchType.ToString().ToLower().TrimEnd('s'), expectedPattern, resultValue));
204 } 273 }
205 else if (matchType.In(MatchType.NotEquals, MatchType.NotContains)) 274 else if (matchType.In(MatchType.NotEquals, MatchType.NotContains))
206 { 275 {
276 // NotEquals, NotContains: the strings are not supposed to match.
277 // So, fail if they do match and pass if they do not.
207 if (match) 278 if (match)
208 return Fail(string.Format("{0} {1} the pattern: {2} ({0}: {3})", description, matchType.ToString().ToLower().Replace("not", ""), expectedPattern, resultValue)); 279 return Fail(string.Format("{0} {1} the pattern: {2} ({0}: {3})", description, matchType.ToString().ToLower().Replace("not", ""), expectedPattern, resultValue));
209 else 280 else
210 return Pass(string.Format("{0} does not {1} the pattern: {2}", description, matchType.ToString().ToLower().TrimEnd('s').Replace("not", ""), expectedPattern)); 281 return Pass(string.Format("{0} does not {1} the pattern: {2}", description, matchType.ToString().ToLower().TrimEnd('s').Replace("not", ""), expectedPattern));
211 } 282 }
212 else 283 else
213 { 284 {
285 // GreaterThan, LessThan
214 if (match) 286 if (match)
215 return Pass(string.Format("{0} ({1}) is {2} {3}", description, resultValue, matchType.ToString().ToLower().Replace("than", " than"), expectedPattern)); 287 return Pass(string.Format("{0} ({1}) is {2} {3}", description, resultValue, matchType.ToString().ToLower().Replace("than", " than"), expectedPattern));
216 else 288 else
217 return Fail(string.Format("{0} ({1}) is not {2} {3}", description, resultValue, matchType.ToString().ToLower().Replace("than", " than"), expectedPattern)); 289 return Fail(string.Format("{0} ({1}) is not {2} {3}", description, resultValue, matchType.ToString().ToLower().Replace("than", " than"), expectedPattern));
218 } 290 }
219 } 291 }
220 292
293 /// <summary>Merges multiple execution results.</summary>
294 /// <param name="results">The results to merge.</param>
295 /// <returns>A single result containing the messages of all the input results, and a failure status if any of the input results failed.</returns>
296 /// <remarks>
297 /// Some checks may want to run several tests on the result of a remote command.
298 /// After collecting their results, they can use this method to combine them into
299 /// a single result that will be reported to the user.
300 /// </remarks>
221 protected CheckResult MergeResults(params CheckResult[] results) 301 protected CheckResult MergeResults(params CheckResult[] results)
222 { 302 {
223 StringBuilder message = new StringBuilder(); 303 StringBuilder message = new StringBuilder();
224 bool failed = false; 304 bool failed = false;
225 foreach (CheckResult result in results) 305 foreach (CheckResult result in results)
226 { 306 {
227 if (result == null) 307 if (result == null)
228 continue; 308 continue;
309 // Report failure if any of the results has failed.
229 if (result.Failed) 310 if (result.Failed)
230 failed = true; 311 failed = true;
312 // Merge the result messages.
231 message.AppendLine(result.Message); 313 message.AppendLine(result.Message);
232 } 314 }
233 return failed ? Fail(message.ToString().Trim()) : Pass(message.ToString().Trim()); 315 return failed ? Fail(message.ToString().Trim()) : Pass(message.ToString().Trim());
234 } 316 }
235 317
318 /// <summary>Executes the check asynchronously.</summary>
319 /// <param name="token">A token for cancelling the execution.</param>
320 /// <returns>The result of the check execution.</returns>
236 protected abstract Task<CheckResult> ExecuteCheckAsync(CancellationToken token); 321 protected abstract Task<CheckResult> ExecuteCheckAsync(CancellationToken token);
237 } 322 }
238 } 323 }