Horizon – Blame information for rev 12

Subversion Repositories:
Rev:
Rev Author Line No. Line
1 office 1 using System;
2 using System.Collections.Generic;
3 using System.Collections.Specialized;
4 using System.ComponentModel;
5 using System.Data.SQLite;
12 office 6 using System.Diagnostics;
1 office 7 using System.Drawing;
8 using System.IO;
9 using System.Linq;
10 using System.Net;
11 using System.Reflection;
12 office 12 using System.Text;
1 office 13 using System.Threading;
14 using System.Threading.Tasks;
5 office 15 using System.Threading.Tasks.Dataflow;
1 office 16 using System.Windows.Forms;
17 using Horizon.Database;
18 using Horizon.Snapshots;
19 using Horizon.Utilities;
20 using Horizon.Utilities.Serialization;
12 office 21 using Mono.Zeroconf;
1 office 22 using NetSparkleUpdater;
23 using NetSparkleUpdater.Enums;
24 using NetSparkleUpdater.SignatureVerifiers;
25 using NetSparkleUpdater.UI.WinForms;
12 office 26 using Newtonsoft.Json;
1 office 27 using Serilog;
28 using TrackedFolders;
12 office 29 using WatsonTcp;
30 using static Horizon.Utilities.Networking.Miscellaneous;
1 office 31 using CaptureMode = Configuration.CaptureMode;
5 office 32 using Path = System.IO.Path;
1 office 33  
34 namespace Horizon
35 {
36 public partial class MainForm : Form
37 {
38 #region Public Enums, Properties and Fields
39  
40 public List<FileSystemWatcher> FileSystemWatchers { get; }
41  
42 public TrackedFolders.TrackedFolders TrackedFolders { get; set; }
43  
44 public Configuration.Configuration Configuration { get; set; }
45  
46 public ScheduledContinuation ChangedConfigurationContinuation { get; set; }
47  
48 #endregion
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  
58 #endregion
59  
60 #region Private Delegates, Events, Enums, Properties, Indexers and Fields
61  
62 private readonly CancellationToken _cancellationToken;
63  
64 private readonly CancellationTokenSource _cancellationTokenSource;
65  
66 private AboutForm _aboutForm;
67  
68 private ManageFoldersForm _manageFoldersForm;
69  
70 private readonly SnapshotDatabase _snapshotDatabase;
71  
72 private SnapshotManagerForm _snapshotManagerForm;
73  
74 private readonly SparkleUpdater _sparkle;
75  
76 private LogViewForm _logViewForm;
77  
78 private readonly LogMemorySink _memorySink;
79  
12 office 80 private RegisterService _horizonDiscoveryService;
81  
82 private WatsonTcpServer _horizonNetworkShare;
83  
1 office 84 public bool MemorySinkEnabled { get; set; }
85  
86 #endregion
87  
88 #region Constructors, Destructors and Finalizers
89  
90 public MainForm(Mutex mutex) : this()
91 {
92 _memorySink = new LogMemorySink();
93  
94 Log.Logger = new LoggerConfiguration()
95 .MinimumLevel.Debug()
96 .WriteTo.Conditional(condition => MemorySinkEnabled, configureSink => configureSink.Sink(_memorySink))
97 .WriteTo.File(Path.Combine(Constants.UserApplicationDirectory, "Logs", $"{Constants.AssemblyName}.log"),
98 rollingInterval: RollingInterval.Day)
99 .CreateLogger();
100  
5 office 101 _snapshotDatabase = new SnapshotDatabase(_cancellationToken);
1 office 102 _snapshotDatabase.SnapshotRevert += SnapshotDatabase_SnapshotRevert;
103 _snapshotDatabase.SnapshotCreate += SnapshotDatabase_SnapshotCreate;
104  
105 TrackedFolders = new TrackedFolders.TrackedFolders();
106 TrackedFolders.Folder.CollectionChanged += Folder_CollectionChanged;
107  
108 // Start application update.
109 var manifestModuleName = Assembly.GetEntryAssembly().ManifestModule.FullyQualifiedName;
110 var icon = Icon.ExtractAssociatedIcon(manifestModuleName);
111  
112 _sparkle = new SparkleUpdater("https://horizon.grimore.org/update/appcast.xml",
113 new Ed25519Checker(SecurityMode.Strict, "LonrgxVjSF0GnY4hzwlRJnLkaxnDn2ikdmOifILzLJY="))
114 {
115 UIFactory = new UIFactory(icon),
116 RelaunchAfterUpdate = true,
117 SecurityProtocolType = SecurityProtocolType.Tls12
118 };
119 _sparkle.StartLoop(true, true);
120 }
121  
122 public MainForm()
123 {
9 office 124 InitializeComponent();
125  
1 office 126 _cancellationTokenSource = new CancellationTokenSource();
127 _cancellationToken = _cancellationTokenSource.Token;
128  
129 _changedFilesLock = new SemaphoreSlim(1, 1);
130 FileSystemWatchers = new List<FileSystemWatcher>();
131  
132 _changedFiles = new HashSet<string>();
133 _changedFilesContinuation = new ScheduledContinuation();
134  
135 ChangedConfigurationContinuation = new ScheduledContinuation();
136 }
137  
138 /// <summary>
139 /// Clean up any resources being used.
140 /// </summary>
141 /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
142 protected override void Dispose(bool disposing)
143 {
144 if (disposing && components != null)
145 {
146 components.Dispose();
147 }
148  
149 _cancellationTokenSource.Cancel();
150  
151 _snapshotDatabase.SnapshotRevert -= SnapshotDatabase_SnapshotRevert;
152 _snapshotDatabase.SnapshotCreate -= SnapshotDatabase_SnapshotCreate;
153  
154 _snapshotDatabase.Dispose();
155  
156 base.Dispose(disposing);
157 }
158  
159 #endregion
160  
161 #region Event Handlers
162  
12 office 163 private void networkSharingToolStripMenuItem_CheckStateChanged(object sender, EventArgs e)
164 {
165 var toolStripMenuItem = (ToolStripMenuItem)sender;
166  
167 switch (toolStripMenuItem.CheckState)
168 {
169 case CheckState.Checked:
170 var freePort = GetAvailableTcpPort();
171  
172 _horizonNetworkShare = new WatsonTcpServer("0.0.0.0", freePort);
173 _horizonNetworkShare.Events.ClientConnected += Events_ClientConnected;
174 _horizonNetworkShare.Events.ClientDisconnected += Events_ClientDisconnected;
175 _horizonNetworkShare.Events.MessageReceived += Events_MessageReceived;
176 #pragma warning disable CS4014
177 _horizonNetworkShare.Start();
178 #pragma warning restore CS4014
179  
180 try
181 {
182 _horizonDiscoveryService = new RegisterService();
183 _horizonDiscoveryService.Name = $"Horizon ({Environment.MachineName})";
184 _horizonDiscoveryService.RegType = "_horizon._tcp";
185 _horizonDiscoveryService.ReplyDomain = "local.";
186 _horizonDiscoveryService.UPort = freePort;
187 _horizonDiscoveryService.Register();
188 }
189 catch (Exception exception)
190 {
191 Log.Error(exception, "Service discovery protocol could not be stared.");
192 }
193  
194 Configuration.NetworkSharing = true;
195 break;
196 case CheckState.Unchecked:
197 if (_horizonNetworkShare != null)
198 {
199 _horizonNetworkShare.Dispose();
200 _horizonNetworkShare = null;
201 }
202  
203 if (_horizonDiscoveryService != null)
204 {
205 _horizonDiscoveryService.Dispose();
206 _horizonDiscoveryService = null;
207  
208 }
209  
210 Configuration.NetworkSharing = false;
211 break;
212 }
213  
214 ChangedConfigurationContinuation.Schedule(TimeSpan.FromSeconds(1),
215 async () => { await SaveConfiguration(); }, _cancellationToken);
216 }
217  
218 private async void Events_MessageReceived(object sender, MessageReceivedEventArgs e)
219 {
220 Log.Information($"Client {e.Client.IpPort} sent {e.Data.Length} bytes via network sharing.");
221  
222 if (e.Data.Length == 0)
223 {
224 return;
225 }
226  
227 try
228 {
229 var payload = Encoding.UTF8.GetString(e.Data);
230  
231 var completeSnapshot = JsonConvert.DeserializeObject<TransferSnapshot>(payload);
232  
233 await _snapshotDatabase.SetCompleteSnapshotAsync(completeSnapshot, _cancellationToken);
234  
235 Log.Information($"Stored {completeSnapshot.Name} from {e.Client.IpPort}");
236 }
237 catch (Exception exception)
238 {
239 Log.Error(exception, $"Failed to process network share from {e.Client.IpPort}.");
240 }
241 }
242  
243 private void Events_ClientDisconnected(object sender, WatsonTcp.DisconnectionEventArgs e)
244 {
245 Log.Information($"Client {e.Client.IpPort} disconnected from network sharing.");
246 }
247  
248 private void Events_ClientConnected(object sender, WatsonTcp.ConnectionEventArgs e)
249 {
250 Log.Information($"Client {e.Client.IpPort} connected to network sharing.");
251 }
252  
1 office 253 private void WindowToolStripMenuItem_Click(object sender, EventArgs e)
254 {
255 windowToolStripMenuItem.Checked = true;
256 screenToolStripMenuItem.Checked = false;
257  
258 Configuration.CaptureMode = CaptureMode.Window;
259  
260 ChangedConfigurationContinuation.Schedule(TimeSpan.FromSeconds(1),
261 async () => { await SaveConfiguration(); }, _cancellationToken);
262 }
263  
264 private void ScreenToolStripMenuItem_Click(object sender, EventArgs e)
265 {
266 screenToolStripMenuItem.Checked = true;
267 windowToolStripMenuItem.Checked = false;
268  
269 Configuration.CaptureMode = CaptureMode.Screen;
270  
271 ChangedConfigurationContinuation.Schedule(TimeSpan.FromSeconds(1),
272 async () => { await SaveConfiguration(); }, _cancellationToken);
273 }
274  
275 private void LogViewToolStripMenuItem_Click(object sender, EventArgs e)
276 {
277 if (_logViewForm != null)
278 {
279 return;
280 }
281  
282 _logViewForm = new LogViewForm(this, _memorySink, _cancellationToken);
283 _logViewForm.Closing += LogViewFormClosing;
284 _logViewForm.Show();
285 }
286  
287 private void LogViewFormClosing(object sender, CancelEventArgs e)
288 {
289 if (_logViewForm == null)
290 {
291 return;
292 }
293  
294 _logViewForm.Closing -= LogViewFormClosing;
295 _logViewForm.Close();
296 _logViewForm = null;
297 }
298  
299 private void SnapshotDatabase_SnapshotCreate(object sender, SnapshotCreateEventArgs e)
300 {
301 switch (e)
302 {
303 case SnapshotCreateSuccessEventArgs snapshotCreateSuccessEventArgs:
304 if (Configuration.ShowBalloonTooltips)
305 {
306 ShowBalloon("Snapshot Succeeded", $"Took a snapshot of {snapshotCreateSuccessEventArgs.Path}.",
307 5000);
308 }
309  
310 Log.Information($"Took a snapshot of {snapshotCreateSuccessEventArgs.Path}.");
311  
312 break;
313 case SnapshotCreateFailureEventArgs snapshotCreateFailureEventArgs:
314 if (Configuration.ShowBalloonTooltips)
315 {
316 ShowBalloon("Snapshot Failed",
317 $"Failed to take a snapshot of {snapshotCreateFailureEventArgs.Path}.", 5000);
318 }
319  
320 Log.Information($"Failed to take a snapshot of {snapshotCreateFailureEventArgs.Path}.");
321  
322 break;
323 }
324 }
325  
326 private void SnapshotDatabase_SnapshotRevert(object sender, SnapshotRevertEventArgs e)
327 {
328 switch (e)
329 {
330 case SnapshotRevertSuccessEventArgs snapshotRevertSuccessEventArgs:
331 if (Configuration.ShowBalloonTooltips)
332 {
333 ShowBalloon("Revert Succeeded", $"File {snapshotRevertSuccessEventArgs.Name} reverted.", 5000);
334 }
335  
336 Log.Information($"File {snapshotRevertSuccessEventArgs.Name} reverted.");
337  
338 break;
339 case SnapshotRevertFailureEventArgs snapshotRevertFailureEventArgs:
340 if (Configuration.ShowBalloonTooltips)
341 {
342 ShowBalloon("Revert Failed", $"Reverting file {snapshotRevertFailureEventArgs.Name} failed.",
343 5000);
344 }
345  
346 Log.Information($"Reverting file {snapshotRevertFailureEventArgs.Name} failed.");
347  
348 break;
349 }
350 }
351  
352 private async void Folder_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
353 {
354 if (e.OldItems != null)
355 {
356 foreach (var item in e.OldItems.OfType<Folder>())
357 {
358 RemoveWatcher(item.Path);
359 }
360 }
361  
362 if (e.NewItems != null)
363 {
364 foreach (var item in e.NewItems.OfType<Folder>())
365 {
366 // If the folder is not enabled then do not add watchers for the path.
367 if (!item.Enable)
368 {
369 continue;
370 }
371  
372 if (Directory.Exists(item.Path))
373 {
374 AddWatcher(item.Path, item.Recursive);
375 }
376 }
377 }
378  
379 await SaveFolders();
380 }
381  
382 private async void MainForm_Load(object sender, EventArgs e)
383 {
384 // Load configuration.
385 Configuration = await LoadConfiguration();
386 launchOnBootToolStripMenuItem.Checked = Configuration.LaunchOnBoot;
387 atomicOperationsToolStripMenuItem.Checked = Configuration.AtomicOperations;
388 enableToolStripMenuItem.Checked = Configuration.Enabled;
389 showBalloonTooltipsToolStripMenuItem.Checked = Configuration.Enabled;
390 windowToolStripMenuItem.Checked = Configuration.CaptureMode == CaptureMode.Window;
391 screenToolStripMenuItem.Checked = Configuration.CaptureMode == CaptureMode.Screen;
12 office 392 networkSharingToolStripMenuItem.Checked = Configuration.NetworkSharing;
1 office 393  
394 // Load all tracked folders.
395 var folders = await LoadFolders();
396  
397 foreach (var folder in folders.Folder)
398 {
399 TrackedFolders.Folder.Add(folder);
400 }
401  
402 ToggleWatchers();
403 }
404  
405 private void ShowBalloonTooltipsToolStripMenuItem_CheckedChanged(object sender, EventArgs e)
406 {
407 Configuration.ShowBalloonTooltips = ((ToolStripMenuItem)sender).Checked;
408  
409 ChangedConfigurationContinuation.Schedule(TimeSpan.FromSeconds(1),
410 async () => { await SaveConfiguration(); }, _cancellationToken);
411 }
412  
413 private void LaunchOnBootToolStripMenuItem_CheckedChanged(object sender, EventArgs e)
414 {
415 Configuration.LaunchOnBoot = ((ToolStripMenuItem)sender).Checked;
416  
417 ChangedConfigurationContinuation.Schedule(TimeSpan.FromSeconds(1),
418 async () => { await SaveConfiguration(); }, _cancellationToken);
419  
420 Miscellaneous.LaunchOnBootSet(Configuration.LaunchOnBoot);
421 }
422  
423 private void AtomicOperationsToolStripMenuItem_CheckedChanged(object sender, EventArgs e)
424 {
425 Configuration.AtomicOperations = ((ToolStripMenuItem)sender).Checked;
426 ChangedConfigurationContinuation.Schedule(TimeSpan.FromSeconds(1),
427 async () => { await SaveConfiguration(); }, _cancellationToken);
428 }
429  
430 private void TrashDatabaseToolStripMenuItem_Click(object sender, EventArgs e)
431 {
432 try
433 {
434 File.Delete(Constants.DatabaseFilePath);
435  
436 if (Configuration.ShowBalloonTooltips)
437 {
438 ShowBalloon("Database Deleted", $"Database file {Constants.DatabaseFilePath} has been deleted.",
439 5000);
440 }
441  
442 Log.Information($"Database file {Constants.DatabaseFilePath} has been deleted.");
443 }
444 catch (Exception exception)
445 {
446 if (Configuration.ShowBalloonTooltips)
447 {
448 ShowBalloon("Could not Delete Database",
449 $"Database file {Constants.DatabaseFilePath} delete failed with error: {exception.Message}",
450 5000);
451 }
452  
453 Log.Information(
454 $"Database file {Constants.DatabaseFilePath} delete failed with error: {exception.Message}");
455 }
456 }
457  
458 private void EnableToolStripMenuItem_CheckedChanged(object sender, EventArgs e)
459 {
460 Configuration.Enabled = enableToolStripMenuItem.Checked;
461  
462 ChangedConfigurationContinuation.Schedule(TimeSpan.FromSeconds(1),
463 async () => { await SaveConfiguration(); }, _cancellationToken);
464  
465 ToggleWatchers();
466 }
467  
468 private void SnapshotsToolStripMenuItem_Click(object sender, EventArgs e)
469 {
470 if (_snapshotManagerForm != null)
471 {
472 return;
473 }
474  
475 _snapshotManagerForm = new SnapshotManagerForm(this, _snapshotDatabase, _cancellationToken);
476 _snapshotManagerForm.Closing += SnapshotManagerFormClosing;
477 _snapshotManagerForm.Show();
478 }
479  
480 private void SnapshotManagerFormClosing(object sender, CancelEventArgs e)
481 {
482 if (_snapshotManagerForm == null)
483 {
484 return;
485 }
486  
487 _snapshotManagerForm.Closing -= SnapshotManagerFormClosing;
488 _snapshotManagerForm.Close();
489 _snapshotManagerForm = null;
490 }
491  
492 private async void FileSystemWatcher_Changed(object sender, FileSystemEventArgs e)
493 {
494 // Ignore directories.
495 if (Directory.Exists(e.FullPath))
496 {
497 return;
498 }
499  
500 await _changedFilesLock.WaitAsync(_cancellationToken);
501 try
502 {
503 var delay = global::TrackedFolders.Constants.Delay;
504 var color = Color.Empty;
505  
506 if (TrackedFolders.TryGet(e.FullPath, out var folder))
507 {
508 delay = folder.Delay;
509 color = folder.Color;
510 }
511  
512 if (_changedFiles.Contains(e.FullPath))
513 {
5 office 514 _changedFilesContinuation.Schedule(delay, async () => await TakeSnapshots(color, _cancellationToken), _cancellationToken);
1 office 515 return;
516 }
517  
518 _changedFiles.Add(e.FullPath);
519  
5 office 520 _changedFilesContinuation.Schedule(delay, async () => await TakeSnapshots(color, _cancellationToken), _cancellationToken);
1 office 521 }
522 catch (Exception exception)
523 {
524 Log.Error(exception, "Could not process changed files.");
525 }
526 finally
527 {
528 _changedFilesLock.Release();
529 }
530 }
531  
532 private void AboutToolStripMenuItem_Click(object sender, EventArgs e)
533 {
534 if (_aboutForm != null)
535 {
536 return;
537 }
538  
539 _aboutForm = new AboutForm(_cancellationToken);
540 _aboutForm.Closing += AboutForm_Closing;
541 _aboutForm.Show();
542 }
543  
544 private void AboutForm_Closing(object sender, CancelEventArgs e)
545 {
546 if (_aboutForm == null)
547 {
548 return;
549 }
550  
551 _aboutForm.Dispose();
552 _aboutForm = null;
553 }
554  
555 private void QuitToolStripMenuItem_Click(object sender, EventArgs e)
556 {
557 Close();
558 }
559  
560 private async void UpdateToolStripMenuItem_Click(object sender, EventArgs e)
561 {
562 // Manually check for updates, this will not show a ui
563 var result = await _sparkle.CheckForUpdatesQuietly();
564 if (result.Status == UpdateStatus.UpdateAvailable)
565 {
566 // if update(s) are found, then we have to trigger the UI to show it gracefully
567 _sparkle.ShowUpdateNeededUI();
568 return;
569 }
570  
571 MessageBox.Show("No updates available at this time.", "Horizon", MessageBoxButtons.OK,
572 MessageBoxIcon.Asterisk,
573 MessageBoxDefaultButton.Button1, MessageBoxOptions.DefaultDesktopOnly, false);
574 }
575  
576 private void NotifyIcon1_Click(object sender, EventArgs e)
577 {
578 if (e is MouseEventArgs mouseEventArgs && mouseEventArgs.Button == MouseButtons.Left)
579 {
580 }
581 }
582  
583 private void ManageFoldersToolStripMenuItem_Click(object sender, EventArgs e)
584 {
585 if (_manageFoldersForm != null)
586 {
587 return;
588 }
589  
590 _manageFoldersForm = new ManageFoldersForm(this, _cancellationToken);
591 _manageFoldersForm.Closing += ManageFoldersForm_Closing;
592 _manageFoldersForm.Show();
593 }
594  
595 private void ManageFoldersForm_Closing(object sender, CancelEventArgs e)
596 {
597 if (_manageFoldersForm == null)
598 {
599 return;
600 }
601  
602 _manageFoldersForm.Closing -= ManageFoldersForm_Closing;
603 _manageFoldersForm.Close();
604 _manageFoldersForm = null;
605 }
606  
607 #endregion
608  
609 #region Public Methods
610  
611 public void ShowBalloon(string title, string text, int time)
612 {
613 notifyIcon1.BalloonTipTitle = title;
614 notifyIcon1.BalloonTipText = text;
615 notifyIcon1.ShowBalloonTip(time);
616 }
617  
618 public async Task TakeSnapshot(string path)
619 {
620 foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.TopDirectoryOnly))
621 {
622 try
623 {
624 var fileName = Path.GetFileName(file);
625 var directory = Path.GetDirectoryName(fileName);
626 var color = Color.Empty;
627 if (TrackedFolders.TryGet(directory, out var folder))
628 {
629 color = folder.Color;
630 }
631  
11 office 632 await _snapshotDatabase.CreateSnapshotAsync(fileName, file, color, _cancellationToken);
1 office 633 }
634 catch (SQLiteException exception)
635 {
636 if (exception.ResultCode == SQLiteErrorCode.Constraint)
637 {
638 Log.Information(exception, "Snapshot already exists.");
639 }
640 }
641 catch (Exception exception)
642 {
643 Log.Error(exception, "Could not take snapshot.", file);
644 }
645 }
646 }
647  
648 public async Task TakeSnapshotRecursive(string path)
649 {
650 foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
651 {
652 try
653 {
654 var fileName = Path.GetFileName(file);
655 var directory = Path.GetDirectoryName(fileName);
656 var color = Color.Empty;
657 if (TrackedFolders.TryGet(directory, out var folder))
658 {
659 color = folder.Color;
660 }
661  
11 office 662 await _snapshotDatabase.CreateSnapshotAsync(fileName, file, color, _cancellationToken);
1 office 663 }
664 catch (SQLiteException exception)
665 {
666 if (exception.ResultCode == SQLiteErrorCode.Constraint)
667 {
668 Log.Information(exception, "Snapshot already exists.");
669 }
670 }
671 catch (Exception exception)
672 {
673 Log.Error(exception, "Could not take snapshot.", file);
674 }
675 }
676 }
677  
678 public async Task SaveConfiguration()
679 {
680 if (!Directory.Exists(Constants.UserApplicationDirectory))
681 {
682 Directory.CreateDirectory(Constants.UserApplicationDirectory);
683 }
684  
685 switch (await Serialization.Serialize(Configuration, Constants.ConfigurationFile, "Configuration",
686 "<!ATTLIST Configuration xmlns:xsi CDATA #IMPLIED xsi:noNamespaceSchemaLocation CDATA #IMPLIED>",
687 CancellationToken.None))
688 {
689 case SerializationSuccess<Configuration.Configuration> _:
690 Log.Information("Serialized configuration.");
691 break;
692 case SerializationFailure serializationFailure:
693 Log.Warning(serializationFailure.Exception.Message, "Failed to serialize configuration.");
694 break;
695 }
696 }
697  
698 public static async Task<Configuration.Configuration> LoadConfiguration()
699 {
700 if (!Directory.Exists(Constants.UserApplicationDirectory))
701 {
702 Directory.CreateDirectory(Constants.UserApplicationDirectory);
703 }
704  
705 var deserializationResult =
706 await Serialization.Deserialize<Configuration.Configuration>(Constants.ConfigurationFile,
707 Constants.ConfigurationNamespace, Constants.ConfigurationXsd, CancellationToken.None);
708  
709 switch (deserializationResult)
710 {
711 case SerializationSuccess<Configuration.Configuration> serializationSuccess:
712 return serializationSuccess.Result;
713 case SerializationFailure serializationFailure:
714 Log.Warning(serializationFailure.Exception, "Failed to load configuration.");
715 return new Configuration.Configuration();
716 default:
717 return new Configuration.Configuration();
718 }
719 }
720  
721 public static async Task<TrackedFolders.TrackedFolders> LoadFolders()
722 {
723 if (!Directory.Exists(Constants.UserApplicationDirectory))
724 {
725 Directory.CreateDirectory(Constants.UserApplicationDirectory);
726 }
727  
728 var deserializationResult =
729 await Serialization.Deserialize<TrackedFolders.TrackedFolders>(Constants.FoldersFile,
730 Constants.TrackedFoldersNamespace, Constants.TrackedFoldersXsd, CancellationToken.None);
731  
732 switch (deserializationResult)
733 {
734 case SerializationSuccess<TrackedFolders.TrackedFolders> serializationSuccess:
735 return serializationSuccess.Result;
736 case SerializationFailure serializationFailure:
737 Log.Warning(serializationFailure.Exception, "Failed to load tracked folders");
738 return new TrackedFolders.TrackedFolders();
739 default:
740 return new TrackedFolders.TrackedFolders();
741 }
742 }
743  
744 public async Task SaveFolders()
745 {
746 if (!Directory.Exists(Constants.UserApplicationDirectory))
747 {
748 Directory.CreateDirectory(Constants.UserApplicationDirectory);
749 }
750  
751 switch (await Serialization.Serialize(TrackedFolders, Constants.FoldersFile, "TrackedFolders",
752 "<!ATTLIST TrackedFolders xmlns:xsi CDATA #IMPLIED xsi:noNamespaceSchemaLocation CDATA #IMPLIED>",
753 CancellationToken.None))
754 {
755 case SerializationSuccess<TrackedFolders.TrackedFolders> _:
756 Log.Information("Serialized tracked folders.");
757 break;
758 case SerializationFailure serializationFailure:
759 Log.Warning(serializationFailure.Exception.Message, "Failed to serialize tracked folders.");
760 break;
761 }
762 }
763  
764 #endregion
765  
766 #region Private Methods
767  
768 private void RemoveWatcher(string folder)
769 {
770 var removeList = new List<FileSystemWatcher>();
771 foreach (var fileSystemWatcher in FileSystemWatchers)
772 {
773 if (fileSystemWatcher.Path.IsPathEqual(folder) ||
774 fileSystemWatcher.Path.IsSubPathOf(folder))
775 {
776 removeList.Add(fileSystemWatcher);
777 }
778 }
779  
780 foreach (var fileSystemWatcher in removeList)
781 {
782 FileSystemWatchers.Remove(fileSystemWatcher);
783 fileSystemWatcher.Changed -= FileSystemWatcher_Changed;
784 fileSystemWatcher.Dispose();
785 }
786 }
787  
788 private void AddWatcher(string folder, bool recursive)
789 {
790 var fileSystemWatcher = new FileSystemWatcher
791 {
792 IncludeSubdirectories = recursive,
793 NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Attributes,
794 Path = folder,
795 EnableRaisingEvents = true
796 };
797  
798 fileSystemWatcher.Changed += FileSystemWatcher_Changed;
799  
800 FileSystemWatchers.Add(fileSystemWatcher);
801 }
802  
803 private void ToggleWatchers()
804 {
805 switch (Configuration.Enabled)
806 {
807 case true:
808 foreach (var watcher in FileSystemWatchers)
809 {
810 watcher.EnableRaisingEvents = true;
811 }
812  
813 if (Configuration.ShowBalloonTooltips)
814 {
815 ShowBalloon("Watching", "Watching folders...", 5000);
816 }
817  
818 Log.Information("Watching folders.");
819  
820 break;
821 default:
822 foreach (var watcher in FileSystemWatchers)
823 {
824 watcher.EnableRaisingEvents = false;
825 }
826  
827 if (Configuration.ShowBalloonTooltips)
828 {
829 ShowBalloon("Not Watching", "Folders are not being watched.", 5000);
830 }
831  
832 Log.Information("Folders are not being watched.");
833  
834 break;
835 }
836 }
837  
5 office 838 private async Task TakeSnapshots(Color color, CancellationToken cancellationToken)
1 office 839 {
5 office 840 var bufferBlock = new BufferBlock<string>(new DataflowBlockOptions() {CancellationToken = cancellationToken});
841 var actionBlock = new ActionBlock<string>(async path =>
1 office 842 {
5 office 843 // In case files have vanished strictly due to the time specified by the tracked folders delay.
844 if (!File.Exists(path))
1 office 845 {
5 office 846 Log.Warning("File vanished after tracked folder delay.", path);
1 office 847  
5 office 848 return;
849 }
1 office 850  
5 office 851 try
852 {
853 var fileName = System.IO.Path.GetFileName(path);
854 var screenCapture = ScreenCapture.Capture((Utilities.CaptureMode)Configuration.CaptureMode);
1 office 855  
11 office 856 await _snapshotDatabase.CreateSnapshotAsync(fileName, path, screenCapture, color,
5 office 857 _cancellationToken);
858 }
859 catch (SQLiteException exception)
860 {
861 if (exception.ResultCode == SQLiteErrorCode.Constraint)
1 office 862 {
5 office 863 Log.Information(exception, "Snapshot already exists.");
1 office 864 }
5 office 865 }
866 catch (Exception exception)
867 {
868 Log.Error(exception, "Could not take snapshot.", path);
869 }
870 });
871  
872 using (var snapshotLink =
873 bufferBlock.LinkTo(actionBlock, new DataflowLinkOptions() { PropagateCompletion = true }))
874 {
875 await _changedFilesLock.WaitAsync(_cancellationToken);
876 try
877 {
878 foreach (var path in _changedFiles)
1 office 879 {
5 office 880 await bufferBlock.SendAsync(path, cancellationToken);
1 office 881 }
5 office 882 bufferBlock.Complete();
883 await bufferBlock.Completion;
1 office 884 }
5 office 885 catch (Exception exception)
886 {
887 Log.Error(exception, "Could not take snapshots.");
888 }
889 finally
890 {
891 _changedFiles.Clear();
892 _changedFilesLock.Release();
893 }
1 office 894 }
895 }
896  
897 #endregion
898 }
899 }