comparison ServerMonitor/Objects/ServerMonitor.cs @ 17:68d7834dc28e

More comments.
author Brad Greco <brad@bgreco.net>
date Sat, 25 May 2019 15:14:26 -0400
parents 052aa62cb42a
children 781d8b980be1
comparison
equal deleted inserted replaced
16:7626b099aefd 17:68d7834dc28e
2 using Renci.SshNet; 2 using Renci.SshNet;
3 using Renci.SshNet.Common; 3 using Renci.SshNet.Common;
4 using ServerMonitorApp.Properties; 4 using ServerMonitorApp.Properties;
5 using System; 5 using System;
6 using System.Collections.Generic; 6 using System.Collections.Generic;
7 using System.Diagnostics;
8 using System.IO; 7 using System.IO;
9 using System.Linq; 8 using System.Linq;
10 using System.Net.NetworkInformation; 9 using System.Net.NetworkInformation;
11 using System.Text;
12 using System.Threading; 10 using System.Threading;
13 using System.Threading.Tasks; 11 using System.Threading.Tasks;
14 using System.Windows.Forms; 12 using System.Windows.Forms;
15 using System.Xml.Serialization; 13 using System.Xml.Serialization;
16 14
17 namespace ServerMonitorApp 15 namespace ServerMonitorApp
18 { 16 {
17 /// <summary>Central class for scheduling and executing checks against remote servers.</summary>
19 public class ServerMonitor 18 public class ServerMonitor
20 { 19 {
21 private readonly string configFileDir; 20 private readonly string configFileDir;
22 private readonly Logger logger; 21 private readonly Logger logger;
22 // Cancellation tokens for executing checks, keyed by check ID.
23 private readonly Dictionary<int, CancellationTokenSource> tokens = new Dictionary<int, CancellationTokenSource>(); 23 private readonly Dictionary<int, CancellationTokenSource> tokens = new Dictionary<int, CancellationTokenSource>();
24 // SSH private keys, keyed by the path to the private key file.
25 // A value of NULL indicates that the private key is inaccessible or encrypted.
24 private readonly Dictionary<string, PrivateKeyFile> privateKeys = new Dictionary<string, PrivateKeyFile>(); 26 private readonly Dictionary<string, PrivateKeyFile> privateKeys = new Dictionary<string, PrivateKeyFile>();
27 // IDs of all checks that have been paused due to network unavailability,
28 // or due to the system being suspended.
29 // Not to be confused with checks that have been disabled by the user.
25 private readonly List<int> pausedChecks = new List<int>(); 30 private readonly List<int> pausedChecks = new List<int>();
26 private bool running, networkAvailable, suspend; 31 private bool running, networkAvailable, suspend;
32 // List of check execution tasks that have been started.
33 // A check task begins by sleeping until the next scheduled execution time,
34 // then executes.
27 private Dictionary<Task<CheckResult>, int> tasks; 35 private Dictionary<Task<CheckResult>, int> tasks;
28 private ServerSummaryForm mainForm; 36 private ServerSummaryForm mainForm;
29 37
30 //private List<Task<CheckResult>> tasks; 38 /// <summary>Fires when the status of a check changes.</summary>
31
32 public event EventHandler<CheckStatusChangedEventArgs> CheckStatusChanged; 39 public event EventHandler<CheckStatusChangedEventArgs> CheckStatusChanged;
33 40
41 /// <summary>The collection of registered servers.</summary>
34 public List<Server> Servers { get; private set; } = new List<Server>(); 42 public List<Server> Servers { get; private set; } = new List<Server>();
35 43
44 /// <summary>A collection of all checks belonging to all registerd servers.</summary>
36 public IEnumerable<Check> Checks => Servers.SelectMany(s => s.Checks); 45 public IEnumerable<Check> Checks => Servers.SelectMany(s => s.Checks);
37 46
47 /// <summary>Path to the file that stores server and check configuration.</summary>
38 public string ConfigFile { get; private set; } 48 public string ConfigFile { get; private set; }
39 49
50 /// <summary>Path to the file that stores server and check configuration.</summary>
40 public IEnumerable<string> LockedKeys { get { return privateKeys.Where(kvp => kvp.Value == null).Select(kvp => kvp.Key); } } 51 public IEnumerable<string> LockedKeys { get { return privateKeys.Where(kvp => kvp.Value == null).Select(kvp => kvp.Key); } }
41 52
53 /// <summary>ServerMonitor constructor.</summary>
54 /// <param name="mainForm">A reference to the main form.</param>
42 public ServerMonitor(ServerSummaryForm mainForm) 55 public ServerMonitor(ServerSummaryForm mainForm)
43 { 56 {
44 this.mainForm = mainForm; 57 this.mainForm = mainForm;
58 // Store configuration in %appdata%\ServerMonitor
45 configFileDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ServerMonitor"); 59 configFileDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ServerMonitor");
46 ConfigFile = Path.Combine(configFileDir, "servers.xml"); 60 ConfigFile = Path.Combine(configFileDir, "servers.xml");
47 logger = new Logger(Path.Combine(configFileDir, "monitor.log")); 61 logger = new Logger(Path.Combine(configFileDir, "monitor.log"));
48 } 62 }
49 63
64 /// <summary>Registers a new server with the server monitor.</summary>
65 /// <param name="server">The server to be added.</param>
50 public void AddServer(Server server) 66 public void AddServer(Server server)
51 { 67 {
52 Servers.Add(server); 68 Servers.Add(server);
53 SaveServers(); 69 SaveServers();
54 } 70 }
55 71
72 /// <summary>Deletes a server from the server monitor.</summary>
73 /// <param name="server">The server to be deleted.</param>
56 public void DeleteServer(Server server) 74 public void DeleteServer(Server server)
57 { 75 {
58 Servers.Remove(server); 76 Servers.Remove(server);
59 SaveServers(); 77 SaveServers();
60 } 78 }
61 79
80 /// <summary>Loads all servers and checks from the config file.</summary>
62 public void LoadServers() 81 public void LoadServers()
63 { 82 {
83 bool triedBackup = false;
84
64 Read: 85 Read:
65 TextReader reader = null; 86 TextReader reader = null;
66 try 87 try
67 { 88 {
68 reader = new StreamReader(ConfigFile); 89 reader = new StreamReader(ConfigFile);
69 XmlSerializer serializer = CreateXmlSerializer(); 90 XmlSerializer serializer = CreateXmlSerializer();
70 Servers.Clear(); 91 Servers.Clear();
71 Servers.AddRange((List<Server>)serializer.Deserialize(reader)); 92 Servers.AddRange((List<Server>)serializer.Deserialize(reader));
72 // Update the Checks so they know what Server they belong to. 93 // Do some more set-up now that the servers and checks have been loaded.
73 // Would rather do this in the Server object on deserialization, but
74 // that doesn't work when using the XML serializer for some reason.
75 foreach (Server server in Servers) 94 foreach (Server server in Servers)
76 { 95 {
96 // Read private keys into memory if they are accessible and not encrypted.
97 // If PrivateKeyFile != null, it means same the key has already been loaded for
98 // a different server and nothing more needs to be done.
77 if (server.LoginType == LoginType.PrivateKey && server.PrivateKeyFile == null) 99 if (server.LoginType == LoginType.PrivateKey && server.PrivateKeyFile == null)
78 OpenPrivateKey(server.KeyFile); 100 OpenPrivateKey(server.KeyFile);
79 foreach (Check check in server.Checks) 101 foreach (Check check in server.Checks)
80 { 102 {
103 // Update the checks so they know what server they belong to.
104 // Would rather do this in the Server object on deserialization, but
105 // that doesn't work when using the XML serializer for some reason.
81 check.Server = server; 106 check.Server = server;
107 // If the program last exited while the check was running, change its status
108 // to the result of its last execution (since, at this point, the check is
109 // not running).
82 if (check.Status == CheckStatus.Running) 110 if (check.Status == CheckStatus.Running)
83 check.Status = check.LastRunStatus; 111 check.Status = check.LastRunStatus;
84 } 112 }
85 server.CheckModified += Server_CheckModified; 113 server.CheckModified += Server_CheckModified;
86 server.EnabledChanged += Server_EnabledChanged; 114 server.EnabledChanged += Server_EnabledChanged;
90 catch (FileNotFoundException) { } 118 catch (FileNotFoundException) { }
91 catch (DirectoryNotFoundException) { } 119 catch (DirectoryNotFoundException) { }
92 catch (InvalidOperationException) 120 catch (InvalidOperationException)
93 { 121 {
94 reader?.Close(); 122 reader?.Close();
95 File.Copy(ConfigFile, ConfigFile + ".error", true); 123 // If there was an error parsing the XML, try again with the backup config file.
96 File.Copy(ConfigFile + ".bak", ConfigFile, true); 124 if (!triedBackup)
97 goto Read; 125 {
126 File.Copy(ConfigFile, ConfigFile + ".error", true);
127 string backupConfig = ConfigFile + ".bak";
128 if (File.Exists(backupConfig))
129 {
130 File.Copy(backupConfig, ConfigFile, true);
131 }
132 triedBackup = true;
133 goto Read;
134 }
135 else
136 {
137 // If there was an error reading the backup file too, give up.
138 throw;
139 }
98 } 140 }
99 finally 141 finally
100 { 142 {
101 reader?.Close(); 143 reader?.Close();
102 } 144 }
103 Application.ApplicationExit += Application_ApplicationExit; 145 Application.ApplicationExit += Application_ApplicationExit;
104 NetworkChange.NetworkAddressChanged += NetworkChange_NetworkAddressChanged; 146 NetworkChange.NetworkAddressChanged += NetworkChange_NetworkAddressChanged;
105 SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged; 147 SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged;
148
149 // Remove old entries from the log file according to user preferences.
106 logger.TrimLog(); 150 logger.TrimLog();
151
107 Run(); 152 Run();
108 } 153 }
109 154
155 /// <summary>Saves all servers and checks to the config file.</summary>
110 public void SaveServers() 156 public void SaveServers()
111 { 157 {
112 GenerateIds(); 158 GenerateIds();
113 TextWriter writer = null; 159 TextWriter writer = null;
114 XmlSerializer serializer = null; 160 XmlSerializer serializer = null;
115 try 161 try
116 { 162 {
163 // Make a backup first in case something goes wrong in the middle of writing.
117 File.Copy(ConfigFile, ConfigFile + ".bak", true); 164 File.Copy(ConfigFile, ConfigFile + ".bak", true);
118 } 165 }
119 catch { } 166 catch { }
120 try 167 try
121 { 168 {
123 serializer = CreateXmlSerializer(); 170 serializer = CreateXmlSerializer();
124 serializer.Serialize(writer, Servers); 171 serializer.Serialize(writer, Servers);
125 } 172 }
126 catch (DirectoryNotFoundException) 173 catch (DirectoryNotFoundException)
127 { 174 {
175 // If the directory does not exist, create it and try again.
128 Directory.CreateDirectory(configFileDir); 176 Directory.CreateDirectory(configFileDir);
129 writer = new StreamWriter(ConfigFile); 177 writer = new StreamWriter(ConfigFile);
130 serializer = CreateXmlSerializer(); 178 serializer = CreateXmlSerializer();
131 serializer.Serialize(writer, Servers); 179 serializer.Serialize(writer, Servers);
132 } 180 }
134 { 182 {
135 writer?.Close(); 183 writer?.Close();
136 } 184 }
137 } 185 }
138 186
187 /// <summary>Main server monitor loop. Schedules and executes checks.</summary>
139 private async void Run() 188 private async void Run()
140 { 189 {
190 // Do not run again if already running or if the system is suspending or resuming.
141 if (running || suspend) 191 if (running || suspend)
142 return; 192 return;
193
143 running = true; 194 running = true;
195
196 // If the network is available, immediately execute checks that were supposed to run
197 // earlier but could not due to network unavailability or the system being suspended.
144 networkAvailable = Helpers.IsNetworkAvailable(); 198 networkAvailable = Helpers.IsNetworkAvailable();
145 if (networkAvailable) 199 if (networkAvailable)
146 { 200 {
147 foreach (int id in pausedChecks) 201 foreach (int id in pausedChecks)
148 { 202 {
149 await ExecuteCheckAsync(Checks.FirstOrDefault(c => c.Id == id)); 203 await ExecuteCheckAsync(Checks.FirstOrDefault(c => c.Id == id));
150 } 204 }
151 pausedChecks.Clear(); 205 pausedChecks.Clear();
152 } 206 }
207
208 // Schedule all checks to run according to their schedules.
209 // Each check will sleep until it is scheduled to run, then execute.
153 tasks = Checks.ToDictionary(c => ScheduleExecuteCheckAsync(c), c => c.Id); 210 tasks = Checks.ToDictionary(c => ScheduleExecuteCheckAsync(c), c => c.Id);
154 while (tasks.Count > 0) 211 while (tasks.Count > 0)
155 { 212 {
213 // When any check is done sleeping and executing, remove the completed
214 // task and queue a new task to schedule it again.
156 Task<CheckResult> task = await Task.WhenAny(tasks.Keys); 215 Task<CheckResult> task = await Task.WhenAny(tasks.Keys);
157 tasks.Remove(task); 216 tasks.Remove(task);
158 try 217 try
159 { 218 {
160 CheckResult result = await task; 219 CheckResult result = await task;
161 // Result will be null if a scheduled check was disabled 220 // Do not schedule the task again if it is now disabled.
221 // Result will be null if a scheduled check was disabled.
162 if (result != null && result.CheckStatus != CheckStatus.Disabled) 222 if (result != null && result.CheckStatus != CheckStatus.Disabled)
163 tasks.Add(ScheduleExecuteCheckAsync(result.Check), result.Check.Id); 223 tasks.Add(ScheduleExecuteCheckAsync(result.Check), result.Check.Id);
164 } 224 }
165 catch (OperationCanceledException) 225 catch (OperationCanceledException)
166 { 226 {
167 227 // When a server's state changes to Disabled, any checks that are executing
168 } 228 // are immediately cancelled. Silently catch these expected exceptions.
169 } 229 }
230 }
231 // If there are no enabled checks scheduled, exit the main loop.
232 // It will be restarted when a check or server is enabled.
170 running = false; 233 running = false;
171 } 234 }
172 235
236 /// <summary>Schedules a check to be run on its schedule.</summary>
237 /// <param name="check">The check to execute.</param>
238 /// <returns>The async check result.</returns>
239 private async Task<CheckResult> ScheduleExecuteCheckAsync(Check check)
240 {
241 // Do not schedule or execute the check if it or its server is disabled.
242 if (!check.Enabled || !check.Server.Enabled)
243 return await Task.FromResult(new CheckResult(check, CheckStatus.Disabled, null));
244
245 // Create a cancellation token that will be used to cancel the check if it or
246 // its server is disabled while it is executing.
247 CancellationTokenSource cts = new CancellationTokenSource();
248 tokens[check.Id] = cts;
249
250 // Sleep until next time the check is supposed to be executed.
251 // Use the LastScheduledRunTime so manual executions by the user do not
252 // interfere with the schedule.
253 check.NextRunTime = check.Schedule.GetNextTime(check.LastScheduledRunTime);
254 int delay = Math.Max(0, (int)(check.NextRunTime - DateTime.Now).TotalMilliseconds);
255 await Task.Delay(delay, cts.Token);
256 check.LastScheduledRunTime = check.NextRunTime;
257
258 // Execute the check if not cancelled.
259 if (!cts.IsCancellationRequested)
260 {
261 // If the network is available, execute the check.
262 // Otherwise, add it to the list of paused checks to be executed
263 // when the network becomes available again.
264 if (networkAvailable)
265 {
266 return await ExecuteCheckAsync(check, cts.Token);
267 }
268 else
269 {
270 if (!pausedChecks.Contains(check.Id))
271 pausedChecks.Add(check.Id);
272 }
273 }
274 return await Task.FromResult(new CheckResult(check, CheckStatus.Disabled, null));
275 }
276
277 /// <summary>Executes a check asynchronously.</summary>
278 /// <param name="check">The check to execute.</param>
279 /// <param name="token">A chancellation token that may be used to cancel the check execution.</param>
280 /// <returns>The async check result.</returns>
173 public async Task<CheckResult> ExecuteCheckAsync(Check check, CancellationToken token = default(CancellationToken)) 281 public async Task<CheckResult> ExecuteCheckAsync(Check check, CancellationToken token = default(CancellationToken))
174 { 282 {
283 // Update the status.
175 check.Status = CheckStatus.Running; 284 check.Status = CheckStatus.Running;
176 OnCheckStatusChanged(check); 285 OnCheckStatusChanged(check);
286
287 // Execute the check.
177 CheckResult result = await check.ExecuteAsync(token); 288 CheckResult result = await check.ExecuteAsync(token);
289
290 // Increment the consecutive failure counter on failue, or reset
291 // the counter on success.
178 if (result.Failed) 292 if (result.Failed)
179 check.ConsecutiveFailures++; 293 check.ConsecutiveFailures++;
294 else
295 check.ConsecutiveFailures = 0;
296
180 OnCheckStatusChanged(check, result); 297 OnCheckStatusChanged(check, result);
181 HandleResultAsync(result); 298 HandleResultAsync(result);
182 return result; 299 return result;
183 } 300 }
184 301
302 /// <summary>Handles the result of a check execution.</summary>
303 /// <param name="result">The result.</param>
185 private void HandleResultAsync(CheckResult result) 304 private void HandleResultAsync(CheckResult result)
186 { 305 {
306 // Log the result.
187 logger.Log(result); 307 logger.Log(result);
308
309 // Notify the user of failure according to user preferences.
310 // If the check succeeded, result.FailAction will be None.
188 if (result.Check.ConsecutiveFailures >= result.Check.MaxConsecutiveFailures) 311 if (result.Check.ConsecutiveFailures >= result.Check.MaxConsecutiveFailures)
189 { 312 {
190 if (result.FailAction == FailAction.FlashTaskbar) 313 if (result.FailAction == FailAction.FlashTaskbar)
191 mainForm.AlertServerForm(result.Check); 314 mainForm.AlertServerForm(result.Check);
192 if (result.FailAction.In(FailAction.FlashTaskbar, FailAction.NotificationBalloon)) 315 if (result.FailAction.In(FailAction.FlashTaskbar, FailAction.NotificationBalloon))
193 mainForm.ShowBalloon(result); 316 mainForm.ShowBalloon(result);
194 } 317 }
195 } 318 }
196 319
320 /// <summary>Reads all check results from the log for a server.</summary>
321 /// <param name="server">The server whose check results should be read.</param>
322 /// <returns>A list of all check results found in the log file for the given server.</returns>
197 public IList<CheckResult> GetLog(Server server) 323 public IList<CheckResult> GetLog(Server server)
198 { 324 {
199 return logger.Read(server); 325 return logger.Read(server);
200 } 326 }
201 327
328 /// <summary>Saves the check settings and notifies event subscribers when the status of a check changes.</summary>
329 /// <param name="check">The check whose status has changed.</param>
330 /// <param name="result">The check result that caused the status to change, if any.</param>
202 private void OnCheckStatusChanged(Check check, CheckResult result = null) 331 private void OnCheckStatusChanged(Check check, CheckResult result = null)
203 { 332 {
204 SaveServers(); 333 SaveServers();
205 CheckStatusChanged?.Invoke(check, new CheckStatusChangedEventArgs(check, result)); 334 CheckStatusChanged?.Invoke(check, new CheckStatusChangedEventArgs(check, result));
206 } 335 }
207 336
337 /// <summary>Handles user modifications to a check's settings.</summary>
338 /// <param name="sender">The check that was modified.</param>
208 private void Server_CheckModified(object sender, EventArgs e) 339 private void Server_CheckModified(object sender, EventArgs e)
209 { 340 {
210 Check check = (Check)sender; 341 Check check = (Check)sender;
211 Task<CheckResult> task = tasks.FirstOrDefault(kvp => kvp.Value == check.Id).Key; 342
343 // No need to mess with the task queue if not currently running.
212 if (running) 344 if (running)
213 { 345 {
346 Task<CheckResult> task = tasks.FirstOrDefault(kvp => kvp.Value == check.Id).Key;
214 if (task == null) 347 if (task == null)
215 { 348 {
216 // No tasks associated with the check, so schedule a new one 349 // No tasks associated with the check, so schedule a new one.
217 tasks.Add(ScheduleExecuteCheckAsync(check), check.Id); 350 tasks.Add(ScheduleExecuteCheckAsync(check), check.Id);
218 } 351 }
219 else 352 else
220 { 353 {
221 // Check was modified or deleted, so remove any waiting tasks 354 // Check was modified or deleted, so remove any waiting tasks.
222 CancelCheck(check); 355 CancelCheck(check);
223 if (check.Server != null) 356 if (check.Server != null)
224 { 357 {
225 // If the check was not deleted, schedule the new check. 358 // If the check was not deleted, schedule the new check.
226 // But only if it's still running, otherwise restarting the monitor below 359 // But only if it's still running, otherwise restarting the monitor below
228 if (running) 361 if (running)
229 tasks.Add(ScheduleExecuteCheckAsync(check), check.Id); 362 tasks.Add(ScheduleExecuteCheckAsync(check), check.Id);
230 } 363 }
231 } 364 }
232 } 365 }
233 // Run again in case removing a task above caused it to stop 366 // Run again in case removing a task above caused it to stop.
234 Run(); 367 Run();
235 } 368 }
236 369
370 /// <summary>Handles the enabled state of a server changing.</summary>
371 /// <param name="sender">The server that was enabled or disabled.</param>
237 private void Server_EnabledChanged(object sender, EventArgs e) 372 private void Server_EnabledChanged(object sender, EventArgs e)
238 { 373 {
239 Server server = (Server)sender; 374 Server server = (Server)sender;
375
376 // Make sure the monitor is running. If no servers were enabled before this
377 // one was enabled, it is not running.
240 if (server.Enabled) 378 if (server.Enabled)
241 { 379 {
242 Run(); 380 Run();
243 } 381 }
244 else 382 else
245 { 383 {
384 // Cancel all queued and executing checks belonging to a
385 // server that was disabled.
246 foreach (Check check in server.Checks) 386 foreach (Check check in server.Checks)
247 { 387 {
248 CancelCheck(check); 388 CancelCheck(check);
249 } 389 }
250 } 390 }
251 } 391 }
252 392
393 /// <summary>Cancels a check that may be executing.</summary>
394 /// <param name="check">The check to cancel.</param>
253 private void CancelCheck(Check check) 395 private void CancelCheck(Check check)
254 { 396 {
255 if (tasks == null) 397 if (tasks == null)
256 return; 398 return;
399
400 // Find the waiting or executing task for the check and remove it.
257 Task<CheckResult> task = tasks.FirstOrDefault(kvp => kvp.Value == check.Id).Key; 401 Task<CheckResult> task = tasks.FirstOrDefault(kvp => kvp.Value == check.Id).Key;
258 if (task != null) 402 if (task != null)
259 tasks.Remove(task); 403 tasks.Remove(task);
404
405 // Remove it from the list of paused checks so it doesn't get restarted later.
260 pausedChecks.RemoveAll(id => id == check.Id); 406 pausedChecks.RemoveAll(id => id == check.Id);
407
408 // Cancel the current execution.
261 if (tokens.TryGetValue(check.Id, out CancellationTokenSource cts)) 409 if (tokens.TryGetValue(check.Id, out CancellationTokenSource cts))
262 cts.Cancel(); 410 cts.Cancel();
263 } 411 }
264 412
265 private async Task<CheckResult> ScheduleExecuteCheckAsync(Check check) 413 /// <summary>Handles network state changing.</summary>
266 {
267 if (!check.Enabled || !check.Server.Enabled)
268 return await Task.FromResult(new CheckResult(check, CheckStatus.Disabled, null));
269
270 CancellationTokenSource cts = new CancellationTokenSource();
271 tokens[check.Id] = cts;
272 check.NextRunTime = check.Schedule.GetNextTime(check.LastScheduledRunTime);
273 int delay = Math.Max(0, (int)(check.NextRunTime - DateTime.Now).TotalMilliseconds);
274 await Task.Delay(delay, cts.Token);
275 check.LastScheduledRunTime = check.NextRunTime;
276 if (networkAvailable && !cts.IsCancellationRequested)
277 {
278 return await ExecuteCheckAsync(check, cts.Token);
279 }
280 else
281 {
282 if (!pausedChecks.Contains(check.Id))
283 pausedChecks.Add(check.Id);
284 return await Task.FromResult(new CheckResult(check, CheckStatus.Disabled, null));
285 }
286 }
287
288 private void NetworkChange_NetworkAddressChanged(object sender, EventArgs e) 414 private void NetworkChange_NetworkAddressChanged(object sender, EventArgs e)
289 { 415 {
290 networkAvailable = Helpers.IsNetworkAvailable(); 416 networkAvailable = Helpers.IsNetworkAvailable();
417 // If the network is available, it might not have been before.
418 // This method is not called from the correct thread, so special
419 // handling is needed to start it on the UI thread again.
291 if (networkAvailable) 420 if (networkAvailable)
292 mainForm.Invoke((MethodInvoker)(() => Run())); 421 mainForm.Invoke((MethodInvoker)(() => Run()));
293 } 422 }
294 423
424 /// <summary>Handles system power mode changes.</summary>
295 private async void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e) 425 private async void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e)
296 { 426 {
427 // If the system is being suspended, cancel all waiting and executing checks.
428 // Once all the checks are removed, the main loop will exit.
297 if (e.Mode == PowerModes.Suspend) 429 if (e.Mode == PowerModes.Suspend)
298 { 430 {
299 foreach (Check check in Checks) 431 foreach (Check check in Checks)
300 { 432 {
301 CancelCheck(check); 433 CancelCheck(check);
302 } 434 }
303 suspend = true; 435 suspend = true;
304 } 436 }
305 else if (e.Mode == PowerModes.Resume) 437 else if (e.Mode == PowerModes.Resume)
306 { 438 {
439 // When resuming from suspend, examine each check to find out if it was
440 // scheduled to be executed during the time period when the systems was
441 // suspended. Add them to the paused checks list, to be executed almost
442 // immediately.
443 // Make sure the list is empty to start.
307 pausedChecks.Clear(); 444 pausedChecks.Clear();
308 foreach (Check check in Checks) 445 foreach (Check check in Checks)
309 { 446 {
310 //CancelCheck(check);
311 if (check.Enabled && check.Server.Enabled && check.NextRunTime < DateTime.Now) 447 if (check.Enabled && check.Server.Enabled && check.NextRunTime < DateTime.Now)
312 { 448 {
313 pausedChecks.Add(check.Id); 449 pausedChecks.Add(check.Id);
314 } 450 }
315 } 451 }
452 // Wait 10 seconds to give things time to quiet down after resuming.
316 await Task.Delay(10000); 453 await Task.Delay(10000);
317 suspend = false; 454 suspend = false;
318 Run(); 455 Run();
319 } 456 }
320 } 457 }
321 458
459 /// <summary>Unregister system events when exiting.</summary>
322 private void Application_ApplicationExit(object sender, EventArgs e) 460 private void Application_ApplicationExit(object sender, EventArgs e)
323 { 461 {
324 NetworkChange.NetworkAddressChanged -= NetworkChange_NetworkAddressChanged; 462 NetworkChange.NetworkAddressChanged -= NetworkChange_NetworkAddressChanged;
325 SystemEvents.PowerModeChanged -= SystemEvents_PowerModeChanged; 463 SystemEvents.PowerModeChanged -= SystemEvents_PowerModeChanged;
326 } 464 }
327 465
466 /// <summary>Attempts to read a private file.</summary>
467 /// <param name="path">The path to the private key file.</param>
468 /// <param name="password">The password used to encrypt the key.</param>
469 /// <returns>A status indicating the result of the attempt.</returns>
328 public KeyStatus OpenPrivateKey(string path, string password = null) 470 public KeyStatus OpenPrivateKey(string path, string password = null)
329 { 471 {
330 KeyStatus keyStatus; 472 KeyStatus keyStatus;
473
331 if (path == null) 474 if (path == null)
332 keyStatus = KeyStatus.NotAccessible; 475 keyStatus = KeyStatus.NotAccessible;
476
477 // Check if the key has already been open and read.
333 if (privateKeys.TryGetValue(path, out PrivateKeyFile key) && key != null) 478 if (privateKeys.TryGetValue(path, out PrivateKeyFile key) && key != null)
334 keyStatus = KeyStatus.Open; 479 keyStatus = KeyStatus.Open;
335 else 480 else
336 { 481 {
337 try 482 try
338 { 483 {
339 key = new PrivateKeyFile(path, password); 484 key = new PrivateKeyFile(path, password);
340 keyStatus = KeyStatus.Open; 485 keyStatus = KeyStatus.Open;
341 } 486 }
487 // If the key is encrypted and the password is empty or incorrect,
488 // return the NeedPassword status.
342 catch (Exception e) when (e is SshPassPhraseNullOrEmptyException || e is InvalidOperationException) 489 catch (Exception e) when (e is SshPassPhraseNullOrEmptyException || e is InvalidOperationException)
343 { 490 {
344 keyStatus = KeyStatus.NeedPassword; 491 keyStatus = KeyStatus.NeedPassword;
345 } 492 }
493 // For any other failure reason, return the NotAccessible status.
346 catch (Exception) 494 catch (Exception)
347 { 495 {
348 keyStatus = KeyStatus.NotAccessible; 496 keyStatus = KeyStatus.NotAccessible;
349 } 497 }
350 } 498 }
499 // A single private key may be used by multiple servers. Update all servers
500 // that use this private key with the results of the above operations.
351 foreach (Server server in Servers) 501 foreach (Server server in Servers)
352 { 502 {
353 if (server.KeyFile == path) 503 if (server.KeyFile == path)
354 { 504 {
355 server.PrivateKeyFile = key; 505 server.PrivateKeyFile = key;
356 server.KeyStatus = keyStatus; 506 server.KeyStatus = keyStatus;
357 } 507 }
358 } 508 }
509 // Keep a reference to this private key so we don't have to re-open
510 // it later if the same key is used on a different server.
359 privateKeys[path] = key; 511 privateKeys[path] = key;
512
360 return keyStatus; 513 return keyStatus;
361 } 514 }
362 515
516 /// <summary>Generates internal IDs for servers and checks.</summary>
363 private void GenerateIds() 517 private void GenerateIds()
364 { 518 {
365 if (Servers.Any()) 519 if (Servers.Any())
366 { 520 {
521 // Start at the maximum ID to make sure IDs are not reused
522 // if a server was deleted so old log entries do not get associated
523 // with a new server.
367 int id = Servers.Max(s => s.Id); 524 int id = Servers.Max(s => s.Id);
368 foreach (Server server in Servers) 525 foreach (Server server in Servers)
369 { 526 {
370 if (server.Id == 0) 527 if (server.Id == 0)
371 server.Id = ++id; 528 server.Id = ++id;
372 } 529 }
373 } 530 }
374 531
375 if (Checks.Any()) 532 if (Checks.Any())
376 { 533 {
534 // Start with the max check ID, same reasons as above.
535 // Is there a reason this is stored in a setting?
377 int id = Math.Max(Settings.Default.MaxCheckId, Checks.Max(c => c.Id)); 536 int id = Math.Max(Settings.Default.MaxCheckId, Checks.Max(c => c.Id));
378 foreach (Check check in Checks) 537 foreach (Check check in Checks)
379 { 538 {
380 if (check.Id == 0) 539 if (check.Id == 0)
381 check.Id = ++id; 540 check.Id = ++id;
383 Settings.Default.MaxCheckId = id; 542 Settings.Default.MaxCheckId = id;
384 Settings.Default.Save(); 543 Settings.Default.Save();
385 } 544 }
386 } 545 }
387 546
547 /// <summary>Creates an XML serializer that can handle servers and all check types.</summary>
548 /// <returns>An XML serializer that can handle servers and all check types.</returns>
388 private XmlSerializer CreateXmlSerializer() 549 private XmlSerializer CreateXmlSerializer()
389 { 550 {
390 return new XmlSerializer(typeof(List<Server>), Check.CheckTypes); 551 return new XmlSerializer(typeof(List<Server>), Check.CheckTypes);
391 } 552 }
392 } 553 }
393 554
555 /// <summary>Event arguments for when a check status changes.</summary>
394 public class CheckStatusChangedEventArgs : EventArgs 556 public class CheckStatusChangedEventArgs : EventArgs
395 { 557 {
558 /// <summary>The check whose status changed.</summary>
396 public Check Check { get; private set; } 559 public Check Check { get; private set; }
397 560
561 /// <summary>The check result that caused the status to change, if any.</summary>
398 public CheckResult CheckResult { get; private set; } 562 public CheckResult CheckResult { get; private set; }
399 563
400 public CheckStatusChangedEventArgs(Check check, CheckResult result) 564 public CheckStatusChangedEventArgs(Check check, CheckResult result)
401 { 565 {
402 Check = check; 566 Check = check;
403 CheckResult = result; 567 CheckResult = result;
404 } 568 }
405 } 569 }
406 570
407 public enum FailAction { FlashTaskbar = 0, NotificationBalloon = 1, None = 10 } 571 /// <summary>Possible actions that may be taken when a check fails.</summary>
572 public enum FailAction
573 {
574 /// <summary>Flashes the Server Monitor tasbar program icon.</summary>
575 FlashTaskbar = 0,
576 /// <summary>Shows a balloon in the notification area.</summary>
577 NotificationBalloon = 1,
578 /// <summary>Take no action.</summary>
579 None = 10
580 }
581
408 } 582 }