Horizon – Blame information for rev 14

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