Horizon – Blame information for rev 17

Subversion Repositories:
Rev:
Rev Author Line No. Line
1 office 1 using System;
10 office 2 using System.Collections.Concurrent;
1 office 3 using System.Collections.Generic;
4 using System.Collections.Specialized;
5 using System.ComponentModel;
6 using System.Data.SQLite;
7 using System.Diagnostics;
8 using System.Drawing;
9 using System.IO;
10 using System.Linq;
11 using System.Net;
7 office 12 using System.Net.Http;
13 using System.Net.Http.Headers;
4 office 14 using System.Net.NetworkInformation;
1 office 15 using System.Reflection;
16 using System.Threading;
17 using System.Threading.Tasks;
18 using System.Threading.Tasks.Dataflow;
4 office 19 using System.Windows;
1 office 20 using System.Windows.Forms;
7 office 21 using Configuration;
1 office 22 using Horizon.Database;
7 office 23 using Horizon.Notifications.Gotify;
1 office 24 using Horizon.Snapshots;
25 using Horizon.Utilities;
26 using Horizon.Utilities.Serialization;
27 using Mono.Zeroconf;
28 using NetSparkleUpdater;
29 using NetSparkleUpdater.Enums;
30 using NetSparkleUpdater.SignatureVerifiers;
31 using NetSparkleUpdater.UI.WinForms;
32 using Newtonsoft.Json;
33 using Serilog;
34 using TrackedFolders;
35 using WatsonTcp;
12 office 36 using Newtonsoft;
1 office 37 using static Horizon.Utilities.Networking.Miscellaneous;
38 using CaptureMode = Configuration.CaptureMode;
39 using Path = System.IO.Path;
12 office 40 using System.Text;
13 office 41 using Tesseract;
42 using System.Runtime.CompilerServices;
43 using System.Text.RegularExpressions;
15 office 44 using Horizon.Searching;
1 office 45  
46 namespace Horizon
47 {
48 public partial class MainForm : Form
49 {
50  
51 #region Static Fields and Constants
52  
53 private static SemaphoreSlim _changedFilesLock;
54  
55 private static HashSet<string> _changedFiles;
56  
57 private static ScheduledContinuation _changedFilesContinuation;
58  
4 office 59 private static readonly LogMemorySink _memorySink = new LogMemorySink();
60  
1 office 61 #endregion
62  
63 #region Private Delegates, Events, Enums, Properties, Indexers and Fields
64  
10 office 65 private ScheduledContinuation _changedConfigurationContinuation;
66  
67 private ScheduledContinuation _trackedFoldersChangedContinuation;
68  
69 private Configuration.Configuration _configuration;
70  
71 private TrackedFolders.TrackedFolders _trackedFolders;
72  
73 private readonly ConcurrentQueue<FileSystemWatcher> _fileSystemWatchers;
74  
1 office 75 private readonly CancellationToken _cancellationToken;
76  
10 office 77 private static HttpClient _httpClient;
7 office 78  
1 office 79 private readonly CancellationTokenSource _cancellationTokenSource;
80  
81 private AboutForm _aboutForm;
82  
83 private ManageFoldersForm _manageFoldersForm;
84  
85 private readonly SnapshotDatabase _snapshotDatabase;
86  
87 private SnapshotManagerForm _snapshotManagerForm;
88  
17 office 89 private SparkleUpdater _sparkle;
1 office 90  
91 private LogViewForm _logViewForm;
92  
12 office 93 private static JsonSerializer _jsonSerializer;
94  
1 office 95 private RegisterService _horizonDiscoveryService;
96  
97 private WatsonTcpServer _horizonNetworkShare;
98  
99 private NotifyFilters _fileSystemWatchersNotifyFilters = NotifyFilters.LastWrite | NotifyFilters.Attributes;
100  
13 office 101 private readonly bool _memorySinkEnabled = true;
1 office 102  
15 office 103 private SearchEngine _searchEngine;
104  
1 office 105 #endregion
106  
107 #region Constructors, Destructors and Finalizers
10 office 108 public MainForm()
109 {
110 InitializeComponent();
1 office 111  
10 office 112 _cancellationTokenSource = new CancellationTokenSource();
113 _cancellationToken = _cancellationTokenSource.Token;
114  
13 office 115 _jsonSerializer = new JsonSerializer();
116  
10 office 117 _httpClient = new HttpClient();
118  
119 _trackedFoldersChangedContinuation = new ScheduledContinuation();
120  
121 _changedFilesLock = new SemaphoreSlim(1, 1);
122 _fileSystemWatchers = new ConcurrentQueue<FileSystemWatcher>();
123  
124 _changedFiles = new HashSet<string>();
125 _changedFilesContinuation = new ScheduledContinuation();
126  
127 _changedConfigurationContinuation = new ScheduledContinuation();
13 office 128  
10 office 129 }
130  
1 office 131 public MainForm(Mutex mutex) : this()
132 {
133 Log.Logger = new LoggerConfiguration()
134 .MinimumLevel.Debug()
13 office 135 .WriteTo.Conditional(condition => _memorySinkEnabled, configureSink => configureSink.Sink(_memorySink))
1 office 136 .WriteTo.File(Path.Combine(Constants.UserApplicationDirectory, "Logs", $"{Constants.AssemblyName}.log"),
137 rollingInterval: RollingInterval.Day)
138 .CreateLogger();
139  
140 _snapshotDatabase = new SnapshotDatabase(_cancellationToken);
141 _snapshotDatabase.SnapshotRevert += SnapshotDatabase_SnapshotRevert;
7 office 142 _snapshotDatabase.SnapshotCreate += SnapshotDatabase_SnapshotCreateAsync;
1 office 143 _snapshotDatabase.SnapshotTransferReceived += SnapshotDatabase_SnapshotTransferReceived;
7 office 144 _snapshotDatabase.SnapshotNoteUpdate += _snapshotDatabase_SnapshotNoteUpdate;
145 _snapshotDatabase.SnapshotDataUpdate += _snapshotDatabase_SnapshotDataUpdate;
1 office 146  
10 office 147 _trackedFolders = new TrackedFolders.TrackedFolders();
148 _trackedFolders.Folder.CollectionChanged += Folder_CollectionChanged;
1 office 149 }
150  
151 /// <summary>
152 /// Clean up any resources being used.
153 /// </summary>
154 /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
155 protected override void Dispose(bool disposing)
156 {
157 if (disposing && components != null)
158 {
159 toolStripMenuItem3.DropDown.Closing -= toolStropMenuItem3DropDown_Closing;
7 office 160 eventsToolStripMenuItem.DropDown.Closing -= eventsToolStripMenuItem_Closing;
161  
1 office 162 components.Dispose();
163 }
164  
165 _cancellationTokenSource.Cancel();
10 office 166 _configuration.PropertyChanged -= _configuration_PropertyChanged;
1 office 167  
7 office 168 _snapshotDatabase.SnapshotNoteUpdate -= _snapshotDatabase_SnapshotNoteUpdate;
169 _snapshotDatabase.SnapshotDataUpdate -= _snapshotDatabase_SnapshotDataUpdate;
1 office 170 _snapshotDatabase.SnapshotRevert -= SnapshotDatabase_SnapshotRevert;
7 office 171 _snapshotDatabase.SnapshotCreate -= SnapshotDatabase_SnapshotCreateAsync;
1 office 172 _snapshotDatabase.SnapshotTransferReceived -= SnapshotDatabase_SnapshotTransferReceived;
173  
174 _snapshotDatabase.Dispose();
175  
176 base.Dispose(disposing);
177 }
178  
7 office 179 private void eventsToolStripMenuItem_Closing(object sender, ToolStripDropDownClosingEventArgs e)
180 {
181 if (e.CloseReason == ToolStripDropDownCloseReason.ItemClicked)
182 {
183 e.Cancel = true;
184 }
185 }
186  
1 office 187 private void toolStropMenuItem3DropDown_Closing(object sender, ToolStripDropDownClosingEventArgs e)
188 {
189 if (e.CloseReason == ToolStripDropDownCloseReason.ItemClicked)
190 {
191 e.Cancel = true;
192 }
193 }
194  
195 #endregion
196  
197 #region Event Handlers
7 office 198  
13 office 199 private void autoNotesToolStripMenuItem_Click(object sender, EventArgs e)
200 {
201 _configuration.AutoNotes = ((ToolStripMenuItem)sender).Checked;
202 }
203  
7 office 204 private void gotfyToolStripTextBox_TextChanged(object sender, EventArgs e)
205 {
206 var toolStripTextBox = ((ToolStripTextBox)sender);
207  
10 office 208 _configuration.GotifyURL = toolStripTextBox.Text;
7 office 209 }
210  
211 private void eventsToolStripMenuItem_CheckStateChanged(object sender, EventArgs e)
212 {
213 var toolStripMenuItem = (ToolStripMenuItem)sender;
214  
215 var text = toolStripMenuItem.Text;
216 var state = toolStripMenuItem.CheckState;
217  
218 foreach (var flag in Enum.GetNames(typeof(NotifyEvent)))
219 {
220 if (string.Equals(flag, text, StringComparison.OrdinalIgnoreCase))
221 {
222 if (Enum.TryParse<NotifyEvent>(flag, true, out var setting))
223 {
224 switch (state)
225 {
226 case CheckState.Checked:
10 office 227 _configuration.NotifyEvents = _configuration.NotifyEvents | setting;
7 office 228 break;
229 case CheckState.Unchecked:
10 office 230 _configuration.NotifyEvents = _configuration.NotifyEvents & ~setting;
7 office 231 break;
232 }
233  
234 }
235 }
236 }
237 }
238  
239 private void gotifyToolStripMenuItem_CheckedChanged(object sender, EventArgs e)
240 {
10 office 241 _configuration.EnableGotify = ((ToolStripMenuItem)sender).Checked;
7 office 242 }
243  
1 office 244 private void attributesToolStripMenuItem_CheckStateChanged(object sender, EventArgs e)
245 {
246 var toolStripMenuItem = (ToolStripMenuItem)sender;
247  
248 var text = toolStripMenuItem.Text;
249 var state = toolStripMenuItem.CheckState;
250  
251 foreach (var flag in Enum.GetNames(typeof(NotifyFilters)))
252 {
253 if (string.Equals(flag, text, StringComparison.OrdinalIgnoreCase))
254 {
255 if (Enum.TryParse<NotifyFilters>(flag, true, out var setting))
256 {
257 switch (state)
258 {
259 case CheckState.Checked:
260 _fileSystemWatchersNotifyFilters = _fileSystemWatchersNotifyFilters | setting;
261 break;
262 case CheckState.Unchecked:
263 _fileSystemWatchersNotifyFilters = _fileSystemWatchersNotifyFilters & ~setting;
264 break;
265 }
266  
267 }
268 }
269 }
270  
10 office 271 _configuration.NotifyFilters = _fileSystemWatchersNotifyFilters;
1 office 272 }
273  
274 private void networkSharingToolStripMenuItem_CheckStateChanged(object sender, EventArgs e)
275 {
276 var toolStripMenuItem = (ToolStripMenuItem)sender;
277  
278 switch (toolStripMenuItem.CheckState)
279 {
280 case CheckState.Checked:
281 var freePort = GetAvailableTcpPort();
282  
283 _horizonNetworkShare = new WatsonTcpServer("0.0.0.0", freePort);
284 _horizonNetworkShare.Events.ClientConnected += Events_ClientConnected;
285 _horizonNetworkShare.Events.ClientDisconnected += Events_ClientDisconnected;
286 _horizonNetworkShare.Events.MessageReceived += Events_MessageReceived;
287 _horizonNetworkShare.Events.ExceptionEncountered += Events_ExceptionEncountered;
288 #pragma warning disable CS4014
289 _horizonNetworkShare.Start();
290 #pragma warning restore CS4014
291  
292 try
293 {
294 _horizonDiscoveryService = new RegisterService();
295 _horizonDiscoveryService.Name = $"Horizon ({Environment.MachineName})";
296 _horizonDiscoveryService.RegType = "_horizon._tcp";
297 _horizonDiscoveryService.ReplyDomain = "local.";
298 _horizonDiscoveryService.UPort = freePort;
299 _horizonDiscoveryService.Register();
300 }
301 catch (Exception exception)
302 {
303 Log.Error(exception, "Service discovery protocol could not be stared.");
304 }
305  
10 office 306 _configuration.NetworkSharing = true;
1 office 307 break;
308 case CheckState.Unchecked:
309 if (_horizonNetworkShare != null)
310 {
311 _horizonNetworkShare.Events.ClientConnected -= Events_ClientConnected;
312 _horizonNetworkShare.Events.ClientDisconnected -= Events_ClientDisconnected;
313 _horizonNetworkShare.Events.MessageReceived -= Events_MessageReceived;
314 _horizonNetworkShare.Events.ExceptionEncountered -= Events_ExceptionEncountered;
315  
316 _horizonNetworkShare.Dispose();
317 _horizonNetworkShare = null;
318 }
319  
320 if (_horizonDiscoveryService != null)
321 {
322 _horizonDiscoveryService.Dispose();
323 _horizonDiscoveryService = null;
324  
325 }
326  
10 office 327 _configuration.NetworkSharing = false;
1 office 328 break;
329 }
330 }
331  
332 private void Events_ExceptionEncountered(object sender, ExceptionEventArgs e)
333 {
334 Log.Error(e.Exception,$"Client threw exception.");
335 }
336  
337 private async void Events_MessageReceived(object sender, MessageReceivedEventArgs e)
338 {
339 Log.Information($"Client {e.Client?.IpPort} sent {e.Data?.Length} bytes via network sharing.");
340  
341 if (e.Data?.Length == 0)
342 {
343 return;
344 }
345  
346 try
347 {
12 office 348 //var payload = Encoding.UTF8.GetString();
1 office 349  
12 office 350 using var memoryStream = new MemoryStream(e.Data);
351 using var streamReader = new StreamReader(memoryStream);
352 using var jsonTextReader = new JsonTextReader(streamReader);
15 office 353 var completeSnapshot = _jsonSerializer.Deserialize<Snapshot>(jsonTextReader);
1 office 354  
15 office 355 await _snapshotDatabase.ApplySnapshotAsync(completeSnapshot, _cancellationToken);
1 office 356  
357 Log.Information($"Stored {completeSnapshot.Name} from {e.Client?.IpPort}");
358 }
359 catch (Exception exception)
360 {
361 Log.Error(exception, $"Failed to process network share from {e.Client?.IpPort}.");
362 }
363 }
364  
365 private void Events_ClientDisconnected(object sender, WatsonTcp.DisconnectionEventArgs e)
366 {
367 Log.Information($"Client {e.Client?.IpPort} disconnected from network sharing.");
368 }
369  
370 private void Events_ClientConnected(object sender, WatsonTcp.ConnectionEventArgs e)
371 {
372 Log.Information($"Client {e.Client?.IpPort} connected to network sharing.");
373 }
374  
375 private void WindowToolStripMenuItem_Click(object sender, EventArgs e)
376 {
377 windowToolStripMenuItem.Checked = true;
378 screenToolStripMenuItem.Checked = false;
379  
10 office 380 _configuration.CaptureMode = CaptureMode.Window;
1 office 381 }
382  
383 private void ScreenToolStripMenuItem_Click(object sender, EventArgs e)
384 {
385 screenToolStripMenuItem.Checked = true;
386 windowToolStripMenuItem.Checked = false;
387  
10 office 388 _configuration.CaptureMode = CaptureMode.Screen;
1 office 389 }
390  
391 private void LogViewToolStripMenuItem_Click(object sender, EventArgs e)
392 {
393 if (_logViewForm != null)
394 {
395 return;
396 }
397  
398 _logViewForm = new LogViewForm(this, _memorySink, _cancellationToken);
399 _logViewForm.Closing += LogViewFormClosing;
400 _logViewForm.Show();
401 }
402  
403 private void LogViewFormClosing(object sender, CancelEventArgs e)
404 {
405 if (_logViewForm == null)
406 {
407 return;
408 }
409  
410 _logViewForm.Closing -= LogViewFormClosing;
411 _logViewForm.Close();
412 _logViewForm = null;
413 }
414  
7 office 415 private async void SnapshotDatabase_SnapshotCreateAsync(object sender, SnapshotCreateEventArgs e)
1 office 416 {
417 switch (e)
418 {
419 case SnapshotCreateSuccessEventArgs snapshotCreateSuccessEventArgs:
13 office 420 var snapshot = snapshotCreateSuccessEventArgs.Snapshot;
10 office 421 if (_configuration.ShowBalloonTooltips)
1 office 422 {
13 office 423 ShowBalloon("Snapshot Created", $"Took a snapshot of {snapshot.Path}.",
1 office 424 5000);
425 }
426  
10 office 427 if(_configuration.EnableGotify && _configuration.NotifyEvents.HasFlag(NotifyEvent.Create))
7 office 428 {
13 office 429 await SendGotifyNotification("Snapshot Created", $"Took a snapshot of {snapshot.Name}.");
7 office 430 }
431  
13 office 432 Log.Information($"Took a snapshot of {snapshot.Path}.");
1 office 433  
434 break;
435 case SnapshotCreateFailureEventArgs snapshotCreateFailureEventArgs:
10 office 436 if (_configuration.ShowBalloonTooltips)
1 office 437 {
438 ShowBalloon("Snapshot Failed",
439 $"Failed to take a snapshot of {snapshotCreateFailureEventArgs.Path}.", 5000);
440 }
441  
442 Log.Information($"Failed to take a snapshot of {snapshotCreateFailureEventArgs.Path}.");
443  
444 break;
445 }
446 }
7 office 447 private async void _snapshotDatabase_SnapshotDataUpdate(object sender, SnapshotDataUpdateEventArgs e)
448 {
449 switch(e)
450 {
451 case SnapshotDataUpdateSuccessEventArgs snapshotDataUpdateSuccessEventArgs:
10 office 452 if (_configuration.EnableGotify && _configuration.NotifyEvents.HasFlag(NotifyEvent.Update))
7 office 453 {
454 await SendGotifyNotification("Snapshot Updated", $"Snapshot data updated {snapshotDataUpdateSuccessEventArgs.NewHash} from {snapshotDataUpdateSuccessEventArgs.OldHash}.");
455 }
456 break;
457 }
458 }
1 office 459  
7 office 460 private async void _snapshotDatabase_SnapshotNoteUpdate(object sender, SnapshotNoteUpdateEventArgs e)
1 office 461 {
7 office 462 switch(e)
463 {
464 case SnapshotNoteUpdateSuccessEventArgs snapshotNoteUpdateSuccessEventArgs:
10 office 465 if (_configuration.EnableGotify && _configuration.NotifyEvents.HasFlag(NotifyEvent.Update))
7 office 466 {
467 await SendGotifyNotification("Snapshot Updated", $"Snapshot note updated for {snapshotNoteUpdateSuccessEventArgs.Hash}.");
468 }
469 break;
470 }
471  
472 }
473  
474 private async void SnapshotDatabase_SnapshotTransferReceived(object sender, SnapshotCreateEventArgs e)
475 {
1 office 476 switch (e)
477 {
478 case SnapshotCreateSuccessEventArgs snapshotCreateSuccessEventArgs:
13 office 479 var snapshot = snapshotCreateSuccessEventArgs.Snapshot;
10 office 480 if (_configuration.ShowBalloonTooltips)
1 office 481 {
13 office 482 ShowBalloon("Snapshot Transfer Success", $"A snapshot has been transferred {snapshot.Path}.",
1 office 483 5000);
484 }
485  
10 office 486 if (_configuration.EnableGotify && _configuration.NotifyEvents.HasFlag(NotifyEvent.Transfer))
7 office 487 {
13 office 488 await SendGotifyNotification("Snapshot Transerred", $"A snapshot has been transferred {snapshot.Name}.");
7 office 489 }
490  
13 office 491 Log.Information($"A snapshot transfer succeeded {snapshot.Path}.");
1 office 492  
493 break;
494 case SnapshotCreateFailureEventArgs snapshotCreateFailureEventArgs:
10 office 495 if (_configuration.ShowBalloonTooltips)
1 office 496 {
497 ShowBalloon("Snapshot Transfer Failure",
498 $"A snapshot failed to transfer {snapshotCreateFailureEventArgs.Path}.", 5000);
499 }
500  
501 Log.Information($"A snapshot failed to transfer {snapshotCreateFailureEventArgs.Path}.");
502  
503 break;
504 }
505 }
506  
7 office 507 private async void SnapshotDatabase_SnapshotRevert(object sender, SnapshotRevertEventArgs e)
1 office 508 {
509 switch (e)
510 {
511 case SnapshotRevertSuccessEventArgs snapshotRevertSuccessEventArgs:
10 office 512 if (_configuration.ShowBalloonTooltips)
1 office 513 {
514 ShowBalloon("Revert Succeeded", $"File {snapshotRevertSuccessEventArgs.Name} reverted.", 5000);
515 }
516  
10 office 517 if (_configuration.EnableGotify && _configuration.NotifyEvents.HasFlag(NotifyEvent.Revert))
7 office 518 {
519 await SendGotifyNotification("Snapshot Reverted", $"Reverted a snapshot of {snapshotRevertSuccessEventArgs.Name}.");
520 }
521  
522  
1 office 523 Log.Information($"File {snapshotRevertSuccessEventArgs.Name} reverted.");
524  
525 break;
526 case SnapshotRevertFailureEventArgs snapshotRevertFailureEventArgs:
10 office 527 if (_configuration.ShowBalloonTooltips)
1 office 528 {
529 ShowBalloon("Revert Failed", $"Reverting file {snapshotRevertFailureEventArgs.Name} failed.",
530 5000);
531 }
532  
533 Log.Information($"Reverting file {snapshotRevertFailureEventArgs.Name} failed.");
534  
535 break;
536 }
537 }
538  
10 office 539 private void Folder_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
1 office 540 {
541 if (e.OldItems != null)
542 {
543 foreach (var item in e.OldItems.OfType<Folder>())
544 {
545 RemoveWatcher(item.Path);
546 }
547 }
548  
549 if (e.NewItems != null)
550 {
551 foreach (var item in e.NewItems.OfType<Folder>())
552 {
553 // If the folder is not enabled then do not add watchers for the path.
554 if (!item.Enable)
555 {
556 continue;
557 }
558  
559 if (Directory.Exists(item.Path))
560 {
561 AddWatcher(item.Path, item.Recursive);
562 }
563 }
564 }
565  
10 office 566 _trackedFoldersChangedContinuation.Schedule(TimeSpan.FromSeconds(1), SaveFolders, _cancellationToken);
1 office 567 }
568  
569 private async void MainForm_Load(object sender, EventArgs e)
570 {
17 office 571 // Start application update.
572 var manifestModuleName = Assembly.GetEntryAssembly().ManifestModule.FullyQualifiedName;
573 var icon = Icon.ExtractAssociatedIcon(manifestModuleName);
574  
575 _sparkle = new SparkleUpdater("https://horizon.grimore.org/update/appcast.xml",
576 new Ed25519Checker(SecurityMode.Strict, "LonrgxVjSF0GnY4hzwlRJnLkaxnDn2ikdmOifILzLJY="))
577 {
578 UIFactory = new UIFactory(icon),
579 RelaunchAfterUpdate = true
580 };
581  
582 await _sparkle.StartLoop(true, true);
583  
8 office 584 // attempt an upgrade
585 #pragma warning disable CS4014
17 office 586 //PerformUpgrade();
8 office 587 #pragma warning restore CS4014
588  
9 office 589 // Set form properties.
590 toolStripMenuItem3.DropDown.Closing += toolStropMenuItem3DropDown_Closing;
591 eventsToolStripMenuItem.DropDown.Closing += eventsToolStripMenuItem_Closing;
592  
1 office 593 // Load configuration.
10 office 594 _configuration = await LoadConfiguration();
595 _configuration.PropertyChanged += _configuration_PropertyChanged;
596  
597 launchOnBootToolStripMenuItem.Checked = _configuration.LaunchOnBoot;
598 atomicOperationsToolStripMenuItem.Checked = _configuration.AtomicOperations;
14 office 599 autoNotesToolStripMenuItem.Checked = _configuration.AutoNotes;
10 office 600 gotifyToolStripMenuItem.Checked = _configuration.EnableGotify;
601 gotifyToolStripTextBox.Text = _configuration.GotifyURL;
602 enableToolStripMenuItem.Checked = _configuration.Enabled;
603 showBalloonTooltipsToolStripMenuItem.Checked = _configuration.Enabled;
604 windowToolStripMenuItem.Checked = _configuration.CaptureMode == CaptureMode.Window;
605 screenToolStripMenuItem.Checked = _configuration.CaptureMode == CaptureMode.Screen;
606 networkSharingToolStripMenuItem.Checked = _configuration.NetworkSharing;
15 office 607  
608 _searchEngine = new SearchEngine(_configuration, _cancellationToken);
609  
1 office 610 foreach (var item in attributesToolStripMenuItem.DropDownItems.OfType<ToolStripMenuItem>())
611 {
612 var text = item.Text;
613  
614 if (Enum.TryParse<NotifyFilters>(text, out var notifyFilter))
615 {
10 office 616 item.Checked = _configuration.NotifyFilters.HasFlag(notifyFilter);
1 office 617 }
618 }
619  
620 // Load all tracked folders.
4 office 621 try
622 {
623 var folders = await LoadFolders();
624 foreach (var folder in folders.Folder)
625 {
10 office 626 _trackedFolders.Folder.Add(folder);
4 office 627 }
1 office 628  
4 office 629 ToggleWatchers();
6 office 630  
631 return;
4 office 632 }
633 catch (FileNotFoundException)
1 office 634 {
4 office 635 ToggleWatchers();
636  
637 return;
1 office 638 }
5 office 639 catch(Exception exception)
640 {
641 Log.Error(exception, "Error loading tracked folders.");
642 }
1 office 643  
4 office 644 if (System.Windows.Forms.MessageBox.Show("Tracked folders could not be loaded, should they be deleted?", "Question", MessageBoxButtons.YesNo) == DialogResult.No)
645 {
646 return;
647 }
648  
1 office 649 ToggleWatchers();
650 }
651  
10 office 652 private void _configuration_PropertyChanged(object sender, PropertyChangedEventArgs e)
653 {
654 _changedConfigurationContinuation.Schedule(TimeSpan.FromSeconds(1), SaveConfiguration, _cancellationToken);
655 }
656  
1 office 657 private void ShowBalloonTooltipsToolStripMenuItem_CheckedChanged(object sender, EventArgs e)
658 {
10 office 659 _configuration.ShowBalloonTooltips = ((ToolStripMenuItem)sender).Checked;
1 office 660 }
661  
662 private void LaunchOnBootToolStripMenuItem_CheckedChanged(object sender, EventArgs e)
663 {
10 office 664 _configuration.LaunchOnBoot = ((ToolStripMenuItem)sender).Checked;
1 office 665  
10 office 666 Miscellaneous.LaunchOnBootSet(_configuration.LaunchOnBoot);
1 office 667 }
668  
669 private void AtomicOperationsToolStripMenuItem_CheckedChanged(object sender, EventArgs e)
670 {
10 office 671 _configuration.AtomicOperations = ((ToolStripMenuItem)sender).Checked;
1 office 672 }
673  
674 private void TrashDatabaseToolStripMenuItem_Click(object sender, EventArgs e)
675 {
676 try
677 {
678 File.Delete(Constants.DatabaseFilePath);
679  
10 office 680 if (_configuration.ShowBalloonTooltips)
1 office 681 {
682 ShowBalloon("Database Deleted", $"Database file {Constants.DatabaseFilePath} has been deleted.",
683 5000);
684 }
685  
686 Log.Information($"Database file {Constants.DatabaseFilePath} has been deleted.");
687 }
688 catch (Exception exception)
689 {
10 office 690 if (_configuration.ShowBalloonTooltips)
1 office 691 {
692 ShowBalloon("Could not Delete Database",
693 $"Database file {Constants.DatabaseFilePath} delete failed with error: {exception.Message}",
694 5000);
695 }
696  
697 Log.Information(
698 $"Database file {Constants.DatabaseFilePath} delete failed with error: {exception.Message}");
699 }
700 }
701  
702 private void EnableToolStripMenuItem_CheckedChanged(object sender, EventArgs e)
703 {
10 office 704 _configuration.Enabled = enableToolStripMenuItem.Checked;
1 office 705  
706 ToggleWatchers();
707 }
708  
709 private void SnapshotsToolStripMenuItem_Click(object sender, EventArgs e)
710 {
711 if (_snapshotManagerForm != null)
712 {
713 return;
714 }
715  
15 office 716 _snapshotManagerForm = new SnapshotManagerForm(_configuration, _trackedFolders, _fileSystemWatchers, _snapshotDatabase, _searchEngine, _cancellationToken);
1 office 717 _snapshotManagerForm.Closing += SnapshotManagerFormClosing;
718 _snapshotManagerForm.Show();
719 }
720  
721 private void SnapshotManagerFormClosing(object sender, CancelEventArgs e)
722 {
723 if (_snapshotManagerForm == null)
724 {
725 return;
726 }
727  
728 _snapshotManagerForm.Closing -= SnapshotManagerFormClosing;
729 _snapshotManagerForm.Close();
730 _snapshotManagerForm = null;
731 }
732  
733 private void FileSystemWatcher_Deleted(object sender, FileSystemEventArgs e)
734 {
735 Log.Information($"File deleted {e.Name}.");
736  
737 //ProcessFilesystemWatcherEvent(e.FullPath);
738 }
739  
740 private void FileSystemWatcher_Created(object sender, FileSystemEventArgs e)
741 {
742 Log.Information($"File created {e.Name}.");
743  
744 ProcessFilesystemWatcherEvent(e.FullPath);
745 }
746  
747 private void FileSystemWatcher_Renamed(object sender, RenamedEventArgs e)
748 {
749 Log.Information($"File renamed from {e.OldName} to {e.Name}.");
750  
751 ProcessFilesystemWatcherEvent(e.FullPath);
752 }
753  
754 private void FileSystemWatcher_Changed(object sender, FileSystemEventArgs e)
755 {
756 Log.Information($"File changed {e.Name}.");
757  
758 ProcessFilesystemWatcherEvent(e.FullPath);
759 }
760  
761 private void ProcessFilesystemWatcherEvent(string path)
762 {
763 // Ignore directories.
764 if (Directory.Exists(path))
765 {
766 return;
767 }
768 #pragma warning disable CS4014
769 Task.Run(async () =>
770 #pragma warning restore CS4014
771 {
772 await _changedFilesLock.WaitAsync(_cancellationToken);
773 try
774 {
775 var delay = global::TrackedFolders.Constants.Delay;
776 var color = Color.Empty;
777  
10 office 778 if (_trackedFolders.TryGet(path, out var folder))
1 office 779 {
780 delay = folder.Delay;
781 color = folder.Color;
782 }
783  
784 if (_changedFiles.Contains(path))
785 {
786 _changedFilesContinuation.Schedule(delay,
787 async () => await TakeSnapshots(color, _cancellationToken), _cancellationToken);
788 return;
789 }
790  
791 _changedFiles.Add(path);
792  
793 _changedFilesContinuation.Schedule(delay,
794 async () => await TakeSnapshots(color, _cancellationToken), _cancellationToken);
795 }
796 catch (Exception exception)
797 {
798 Log.Error(exception, "Could not process changed files.");
799 }
800 finally
801 {
802 _changedFilesLock.Release();
803 }
804 }, CancellationToken.None);
805 }
806  
807 private void AboutToolStripMenuItem_Click(object sender, EventArgs e)
808 {
809 if (_aboutForm != null)
810 {
811 return;
812 }
813  
814 _aboutForm = new AboutForm(_cancellationToken);
815 _aboutForm.Closing += AboutForm_Closing;
816 _aboutForm.Show();
817 }
818  
819 private void AboutForm_Closing(object sender, CancelEventArgs e)
820 {
821 if (_aboutForm == null)
822 {
823 return;
824 }
825  
826 _aboutForm.Dispose();
827 _aboutForm = null;
828 }
829  
830 private void QuitToolStripMenuItem_Click(object sender, EventArgs e)
831 {
832 Close();
833 }
834  
835 private async void UpdateToolStripMenuItem_Click(object sender, EventArgs e)
836 {
8 office 837 await PerformUpgrade();
1 office 838 }
839  
840 private void NotifyIcon1_Click(object sender, EventArgs e)
841 {
842 if (e is MouseEventArgs mouseEventArgs && mouseEventArgs.Button == MouseButtons.Left)
843 {
844 }
845 }
846  
847 private void ManageFoldersToolStripMenuItem_Click(object sender, EventArgs e)
848 {
849 if (_manageFoldersForm != null)
850 {
851 return;
852 }
853  
15 office 854 _manageFoldersForm = new ManageFoldersForm(_configuration, _trackedFolders, _snapshotDatabase, _searchEngine, _cancellationToken);
1 office 855 _manageFoldersForm.Closing += ManageFoldersForm_Closing;
856 _manageFoldersForm.Show();
857 }
858  
859 private void ManageFoldersForm_Closing(object sender, CancelEventArgs e)
860 {
861 if (_manageFoldersForm == null)
862 {
863 return;
864 }
865  
866 _manageFoldersForm.Closing -= ManageFoldersForm_Closing;
867 _manageFoldersForm.Close();
868 _manageFoldersForm = null;
869 }
870  
871 #endregion
872  
873 #region Public Methods
874  
875 public void ShowBalloon(string title, string text, int time)
876 {
877 notifyIcon1.BalloonTipTitle = title;
878 notifyIcon1.BalloonTipText = text;
879 notifyIcon1.ShowBalloonTip(time);
880 }
881  
882 public async Task SaveConfiguration()
883 {
884 if (!Directory.Exists(Constants.UserApplicationDirectory))
885 {
886 Directory.CreateDirectory(Constants.UserApplicationDirectory);
887 }
888  
10 office 889 switch (await Serialization.Serialize(_configuration, Constants.ConfigurationFile, "Configuration",
1 office 890 "<!ATTLIST Configuration xmlns:xsi CDATA #IMPLIED xsi:noNamespaceSchemaLocation CDATA #IMPLIED>",
891 CancellationToken.None))
892 {
893 case SerializationSuccess<Configuration.Configuration> _:
894 Log.Information("Serialized configuration.");
895 break;
896 case SerializationFailure serializationFailure:
897 Log.Warning(serializationFailure.Exception.Message, "Failed to serialize configuration.");
898 break;
899 }
900 }
901  
902 public static async Task<Configuration.Configuration> LoadConfiguration()
903 {
904 if (!Directory.Exists(Constants.UserApplicationDirectory))
905 {
906 Directory.CreateDirectory(Constants.UserApplicationDirectory);
907 }
908  
909 var deserializationResult =
910 await Serialization.Deserialize<Configuration.Configuration>(Constants.ConfigurationFile,
911 Constants.ConfigurationNamespace, Constants.ConfigurationXsd, CancellationToken.None);
912  
913 switch (deserializationResult)
914 {
915 case SerializationSuccess<Configuration.Configuration> serializationSuccess:
916 return serializationSuccess.Result;
917 case SerializationFailure serializationFailure:
918 Log.Warning(serializationFailure.Exception, "Failed to load configuration.");
919 return new Configuration.Configuration();
920 default:
921 return new Configuration.Configuration();
922 }
923 }
924  
925 public static async Task<TrackedFolders.TrackedFolders> LoadFolders()
926 {
927 if (!Directory.Exists(Constants.UserApplicationDirectory))
928 {
929 Directory.CreateDirectory(Constants.UserApplicationDirectory);
930 }
931  
932 var deserializationResult =
933 await Serialization.Deserialize<TrackedFolders.TrackedFolders>(Constants.FoldersFile,
934 Constants.TrackedFoldersNamespace, Constants.TrackedFoldersXsd, CancellationToken.None);
935  
936 switch (deserializationResult)
937 {
938 case SerializationSuccess<TrackedFolders.TrackedFolders> serializationSuccess:
939 return serializationSuccess.Result;
940 case SerializationFailure serializationFailure:
941 Log.Warning(serializationFailure.Exception, "Failed to load tracked folders");
4 office 942 throw serializationFailure.Exception;
1 office 943 }
4 office 944  
945 return null;
1 office 946 }
947  
948 public async Task SaveFolders()
949 {
950 if (!Directory.Exists(Constants.UserApplicationDirectory))
951 {
952 Directory.CreateDirectory(Constants.UserApplicationDirectory);
953 }
954  
10 office 955 switch (await Serialization.Serialize(_trackedFolders, Constants.FoldersFile, "TrackedFolders",
1 office 956 "<!ATTLIST TrackedFolders xmlns:xsi CDATA #IMPLIED xsi:noNamespaceSchemaLocation CDATA #IMPLIED>",
957 CancellationToken.None))
958 {
959 case SerializationSuccess<TrackedFolders.TrackedFolders> _:
960 Log.Information("Serialized tracked folders.");
961 break;
962 case SerializationFailure serializationFailure:
963 Log.Warning(serializationFailure.Exception.Message, "Failed to serialize tracked folders.");
964 break;
965 }
966 }
967  
968 #endregion
969  
970 #region Private Methods
971  
8 office 972 private async Task PerformUpgrade()
973 {
974 // Manually check for updates, this will not show a ui
975 var updateCheck = await _sparkle.CheckForUpdatesQuietly();
976 switch (updateCheck.Status)
977 {
978 case UpdateStatus.UserSkipped:
979 var assemblyVersion = Assembly.GetExecutingAssembly().GetName().Version;
980 updateCheck.Updates.Sort(UpdateComparer);
981 var latestVersion = updateCheck.Updates.FirstOrDefault();
982 if (latestVersion != null)
983 {
984 var availableVersion = new Version(latestVersion.Version);
985  
986 if (availableVersion <= assemblyVersion)
987 {
988 return;
989 }
990 }
991  
992 // Only offer an update nag screen if the version is one month old since skipping an update.
993 if (DateTime.Now.Subtract(latestVersion.PublicationDate).TotalDays < 30)
994 {
995 return;
996 }
997  
998 var decision = System.Windows.Forms.MessageBox.Show(
999 "Update available but it has been previously skipped and a month has passed since. Should the update proceed now?",
1000 Assembly.GetExecutingAssembly().GetName().Name, MessageBoxButtons.YesNo,
1001 MessageBoxIcon.Asterisk,
1002 MessageBoxDefaultButton.Button1, System.Windows.Forms.MessageBoxOptions.DefaultDesktopOnly, false);
1003  
1004 if (decision.HasFlag(DialogResult.No))
1005 {
1006 return;
1007 }
1008  
1009 goto default;
1010 case UpdateStatus.UpdateNotAvailable:
1011 System.Windows.Forms.MessageBox.Show("No updates available at this time.",
1012 Assembly.GetExecutingAssembly().GetName().Name, MessageBoxButtons.OK,
1013 MessageBoxIcon.Asterisk,
1014 MessageBoxDefaultButton.Button1, System.Windows.Forms.MessageBoxOptions.DefaultDesktopOnly, false);
1015 break;
1016 case UpdateStatus.CouldNotDetermine:
1017 Log.Error("Could not determine the update availability status.");
1018 break;
1019 default:
1020 _sparkle.ShowUpdateNeededUI();
1021 break;
1022 }
1023 }
1024  
1025 private static int UpdateComparer(AppCastItem x, AppCastItem y)
1026 {
1027 if (x == null)
1028 {
1029 return 1;
1030 }
1031  
1032 if (y == null)
1033 {
1034 return -1;
1035 }
1036  
1037 if (x == y)
1038 {
1039 return 0;
1040 }
1041  
1042 return new Version(y.Version).CompareTo(new Version(x.Version));
1043 }
1044  
1 office 1045 private void RemoveWatcher(string folder)
1046 {
10 office 1047 var count = _fileSystemWatchers.Count;
1048 while (_fileSystemWatchers.TryDequeue(out var fileSystemWatcher))
1 office 1049 {
10 office 1050 if(--count == 0)
1051 {
1052 break;
1053 }
1054  
1055 if (fileSystemWatcher.Path.IsPathEqual(folder) ||
1 office 1056 fileSystemWatcher.Path.IsSubPathOf(folder))
1057 {
10 office 1058 continue;
1 office 1059 }
10 office 1060  
1061 _fileSystemWatchers.Enqueue(fileSystemWatcher);
1 office 1062 }
1063  
1064 }
1065  
1066 private void AddWatcher(string folder, bool recursive)
1067 {
1068 var fileSystemWatcher = new FileSystemWatcher
1069 {
1070 IncludeSubdirectories = recursive,
1071 NotifyFilter = _fileSystemWatchersNotifyFilters,
1072 Path = folder,
1073 EnableRaisingEvents = true,
1074 InternalBufferSize = 65536
1075 };
1076  
1077 fileSystemWatcher.Changed += FileSystemWatcher_Changed;
1078 fileSystemWatcher.Renamed += FileSystemWatcher_Renamed;
1079 fileSystemWatcher.Created += FileSystemWatcher_Created;
1080 fileSystemWatcher.Deleted += FileSystemWatcher_Deleted;
1081  
10 office 1082 _fileSystemWatchers.Enqueue(fileSystemWatcher);
1 office 1083 }
1084  
1085 private void ToggleWatchers()
1086 {
10 office 1087 switch (_configuration.Enabled)
1 office 1088 {
1089 case true:
10 office 1090 foreach (var watcher in _fileSystemWatchers)
1 office 1091 {
1092 watcher.EnableRaisingEvents = true;
1093 }
1094  
10 office 1095 if (_configuration.ShowBalloonTooltips)
1 office 1096 {
1097 ShowBalloon("Watching", "Watching folders...", 5000);
1098 }
1099  
1100 Log.Information("Watching folders.");
1101  
1102 break;
1103 default:
10 office 1104 foreach (var watcher in _fileSystemWatchers)
1 office 1105 {
1106 watcher.EnableRaisingEvents = false;
1107 }
1108  
10 office 1109 if (_configuration.ShowBalloonTooltips)
1 office 1110 {
1111 ShowBalloon("Not Watching", "Folders are not being watched.", 5000);
1112 }
1113  
1114 Log.Information("Folders are not being watched.");
1115  
1116 break;
1117 }
1118 }
1119  
1120 private async Task TakeSnapshots(Color color, CancellationToken cancellationToken)
1121 {
13 office 1122  
1 office 1123 var bufferBlock = new BufferBlock<string>(new DataflowBlockOptions() {CancellationToken = cancellationToken});
1124 var actionBlock = new ActionBlock<string>(async path =>
1125 {
1126 // In case files have vanished strictly due to the time specified by the tracked folders delay.
1127 if (!File.Exists(path))
1128 {
1129 Log.Warning($"File vanished after tracked folder delay: {path}");
1130  
1131 return;
1132 }
1133  
1134 try
1135 {
13 office 1136 var fileName = Path.GetFileName(path);
10 office 1137 var screenCapture = ScreenCapture.Capture((Utilities.CaptureMode)_configuration.CaptureMode);
1 office 1138  
15 office 1139 var terms = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
1140  
1141 if (_configuration.AutoNotes)
13 office 1142 {
1143 await foreach (var term in Extensions.RecognizeStrings(screenCapture, cancellationToken))
1144 {
1145 terms.Add(term);
1146 }
15 office 1147 }
13 office 1148  
15 office 1149 var note = string.Join(" ", terms);
1150  
1151 // async, decompose, branch
1152 if (await _snapshotDatabase.CreateSnapshotAsync(fileName, path, screenCapture, color, note, cancellationToken) is Snapshot snapshot)
1153 {
1154 await _searchEngine.Index(snapshot, cancellationToken);
13 office 1155 }
1 office 1156 }
1157 catch (SQLiteException exception)
1158 {
1159 if (exception.ResultCode == SQLiteErrorCode.Constraint)
1160 {
1161 Log.Information(exception, "Snapshot already exists.");
1162 }
1163 }
1164 catch (Exception exception)
1165 {
1166 Log.Error(exception, $"Could not take snapshot of file {path}");
1167 }
1168 });
1169  
13 office 1170 using var snapshotLink = bufferBlock.LinkTo(actionBlock, new DataflowLinkOptions() { PropagateCompletion = true });
1171  
1172 await _changedFilesLock.WaitAsync(_cancellationToken);
1173 try
1 office 1174 {
13 office 1175 foreach (var path in _changedFiles)
1 office 1176 {
13 office 1177 await bufferBlock.SendAsync(path, cancellationToken);
1 office 1178 }
13 office 1179 bufferBlock.Complete();
1180 await bufferBlock.Completion;
1 office 1181 }
13 office 1182 catch (Exception exception)
1183 {
1184 Log.Error(exception, "Could not take snapshots.");
1185 }
1186 finally
1187 {
1188 _changedFiles.Clear();
1189 _changedFilesLock.Release();
1190 }
1 office 1191 }
1192  
7 office 1193 private async Task SendGotifyNotification(string v1, string v2)
1194 {
10 office 1195 if (!Uri.TryCreate(_configuration.GotifyURL, UriKind.RelativeOrAbsolute, out var uri))
7 office 1196 {
1197 Log.Warning($"Invalid Gotify URL provided.");
1198 return;
1199 }
1200 var gotifyMessageSending = new GotifyMessageOutgoing()
1201 {
1202 Title = v1,
1203 Message = v2
1204 };
1205  
1206 var payload = JsonConvert.SerializeObject(gotifyMessageSending);
1207 using var stringContent = new StringContent(payload, Encoding.UTF8, "application/json");
1208 using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, uri);
1209 httpRequestMessage.Content = stringContent;
1210 var response = await _httpClient.SendAsync(httpRequestMessage, _cancellationToken);
1211 var responseContent = await response.Content.ReadAsStringAsync();
1212 var gotifyReply = JsonConvert.DeserializeObject<GotifyMessageIncoming>(responseContent);
1213 if (gotifyReply?.AppId == null)
1214 {
1215 Log.Error($"Failed Sending notification.");
1216 }
1217 }
1218  
1 office 1219 #endregion
13 office 1220  
1221  
1 office 1222 }
1223 }