Mercurial > servermonitor
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 } |