Horizon – Rev 11
?pathlinks?
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Data.SQLite;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using System.Windows.Forms;
using Horizon.Database;
using Horizon.Snapshots;
using Horizon.Utilities;
using Horizon.Utilities.Serialization;
using NetSparkleUpdater;
using NetSparkleUpdater.Enums;
using NetSparkleUpdater.SignatureVerifiers;
using NetSparkleUpdater.UI.WinForms;
using Serilog;
using TrackedFolders;
using CaptureMode = Configuration.CaptureMode;
using Path = System.IO.Path;
namespace Horizon
{
public partial class MainForm : Form
{
#region Public Enums, Properties and Fields
public List<FileSystemWatcher> FileSystemWatchers { get; }
public TrackedFolders.TrackedFolders TrackedFolders { get; set; }
public Configuration.Configuration Configuration { get; set; }
public ScheduledContinuation ChangedConfigurationContinuation { get; set; }
#endregion
#region Static Fields and Constants
private static SemaphoreSlim _changedFilesLock;
private static HashSet<string> _changedFiles;
private static ScheduledContinuation _changedFilesContinuation;
#endregion
#region Private Delegates, Events, Enums, Properties, Indexers and Fields
private readonly CancellationToken _cancellationToken;
private readonly CancellationTokenSource _cancellationTokenSource;
private AboutForm _aboutForm;
private ManageFoldersForm _manageFoldersForm;
private readonly SnapshotDatabase _snapshotDatabase;
private SnapshotManagerForm _snapshotManagerForm;
private readonly SparkleUpdater _sparkle;
private LogViewForm _logViewForm;
private readonly LogMemorySink _memorySink;
public bool MemorySinkEnabled { get; set; }
#endregion
#region Constructors, Destructors and Finalizers
public MainForm(Mutex mutex) : this()
{
_memorySink = new LogMemorySink();
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Conditional(condition => MemorySinkEnabled, configureSink => configureSink.Sink(_memorySink))
.WriteTo.File(Path.Combine(Constants.UserApplicationDirectory, "Logs", $"{Constants.AssemblyName}.log"),
rollingInterval: RollingInterval.Day)
.CreateLogger();
_snapshotDatabase = new SnapshotDatabase(_cancellationToken);
_snapshotDatabase.SnapshotRevert += SnapshotDatabase_SnapshotRevert;
_snapshotDatabase.SnapshotCreate += SnapshotDatabase_SnapshotCreate;
TrackedFolders = new TrackedFolders.TrackedFolders();
TrackedFolders.Folder.CollectionChanged += Folder_CollectionChanged;
// Start application update.
var manifestModuleName = Assembly.GetEntryAssembly().ManifestModule.FullyQualifiedName;
var icon = Icon.ExtractAssociatedIcon(manifestModuleName);
_sparkle = new SparkleUpdater("https://horizon.grimore.org/update/appcast.xml",
new Ed25519Checker(SecurityMode.Strict, "LonrgxVjSF0GnY4hzwlRJnLkaxnDn2ikdmOifILzLJY="))
{
UIFactory = new UIFactory(icon),
RelaunchAfterUpdate = true,
SecurityProtocolType = SecurityProtocolType.Tls12
};
_sparkle.StartLoop(true, true);
}
public MainForm()
{
InitializeComponent();
_cancellationTokenSource = new CancellationTokenSource();
_cancellationToken = _cancellationTokenSource.Token;
_changedFilesLock = new SemaphoreSlim(1, 1);
FileSystemWatchers = new List<FileSystemWatcher>();
_changedFiles = new HashSet<string>();
_changedFilesContinuation = new ScheduledContinuation();
ChangedConfigurationContinuation = new ScheduledContinuation();
}
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && components != null)
{
components.Dispose();
}
_cancellationTokenSource.Cancel();
_snapshotDatabase.SnapshotRevert -= SnapshotDatabase_SnapshotRevert;
_snapshotDatabase.SnapshotCreate -= SnapshotDatabase_SnapshotCreate;
_snapshotDatabase.Dispose();
base.Dispose(disposing);
}
#endregion
#region Event Handlers
private void WindowToolStripMenuItem_Click(object sender, EventArgs e)
{
windowToolStripMenuItem.Checked = true;
screenToolStripMenuItem.Checked = false;
Configuration.CaptureMode = CaptureMode.Window;
ChangedConfigurationContinuation.Schedule(TimeSpan.FromSeconds(1),
async () => { await SaveConfiguration(); }, _cancellationToken);
}
private void ScreenToolStripMenuItem_Click(object sender, EventArgs e)
{
screenToolStripMenuItem.Checked = true;
windowToolStripMenuItem.Checked = false;
Configuration.CaptureMode = CaptureMode.Screen;
ChangedConfigurationContinuation.Schedule(TimeSpan.FromSeconds(1),
async () => { await SaveConfiguration(); }, _cancellationToken);
}
private void LogViewToolStripMenuItem_Click(object sender, EventArgs e)
{
if (_logViewForm != null)
{
return;
}
_logViewForm = new LogViewForm(this, _memorySink, _cancellationToken);
_logViewForm.Closing += LogViewFormClosing;
_logViewForm.Show();
}
private void LogViewFormClosing(object sender, CancelEventArgs e)
{
if (_logViewForm == null)
{
return;
}
_logViewForm.Closing -= LogViewFormClosing;
_logViewForm.Close();
_logViewForm = null;
}
private void SnapshotDatabase_SnapshotCreate(object sender, SnapshotCreateEventArgs e)
{
switch (e)
{
case SnapshotCreateSuccessEventArgs snapshotCreateSuccessEventArgs:
if (Configuration.ShowBalloonTooltips)
{
ShowBalloon("Snapshot Succeeded", $"Took a snapshot of {snapshotCreateSuccessEventArgs.Path}.",
5000);
}
Log.Information($"Took a snapshot of {snapshotCreateSuccessEventArgs.Path}.");
break;
case SnapshotCreateFailureEventArgs snapshotCreateFailureEventArgs:
if (Configuration.ShowBalloonTooltips)
{
ShowBalloon("Snapshot Failed",
$"Failed to take a snapshot of {snapshotCreateFailureEventArgs.Path}.", 5000);
}
Log.Information($"Failed to take a snapshot of {snapshotCreateFailureEventArgs.Path}.");
break;
}
}
private void SnapshotDatabase_SnapshotRevert(object sender, SnapshotRevertEventArgs e)
{
switch (e)
{
case SnapshotRevertSuccessEventArgs snapshotRevertSuccessEventArgs:
if (Configuration.ShowBalloonTooltips)
{
ShowBalloon("Revert Succeeded", $"File {snapshotRevertSuccessEventArgs.Name} reverted.", 5000);
}
Log.Information($"File {snapshotRevertSuccessEventArgs.Name} reverted.");
break;
case SnapshotRevertFailureEventArgs snapshotRevertFailureEventArgs:
if (Configuration.ShowBalloonTooltips)
{
ShowBalloon("Revert Failed", $"Reverting file {snapshotRevertFailureEventArgs.Name} failed.",
5000);
}
Log.Information($"Reverting file {snapshotRevertFailureEventArgs.Name} failed.");
break;
}
}
private async void Folder_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.OldItems != null)
{
foreach (var item in e.OldItems.OfType<Folder>())
{
RemoveWatcher(item.Path);
}
}
if (e.NewItems != null)
{
foreach (var item in e.NewItems.OfType<Folder>())
{
// If the folder is not enabled then do not add watchers for the path.
if (!item.Enable)
{
continue;
}
if (Directory.Exists(item.Path))
{
AddWatcher(item.Path, item.Recursive);
}
}
}
await SaveFolders();
}
private async void MainForm_Load(object sender, EventArgs e)
{
// Load configuration.
Configuration = await LoadConfiguration();
launchOnBootToolStripMenuItem.Checked = Configuration.LaunchOnBoot;
atomicOperationsToolStripMenuItem.Checked = Configuration.AtomicOperations;
enableToolStripMenuItem.Checked = Configuration.Enabled;
showBalloonTooltipsToolStripMenuItem.Checked = Configuration.Enabled;
windowToolStripMenuItem.Checked = Configuration.CaptureMode == CaptureMode.Window;
screenToolStripMenuItem.Checked = Configuration.CaptureMode == CaptureMode.Screen;
// Load all tracked folders.
var folders = await LoadFolders();
foreach (var folder in folders.Folder)
{
TrackedFolders.Folder.Add(folder);
}
ToggleWatchers();
}
private void ShowBalloonTooltipsToolStripMenuItem_CheckedChanged(object sender, EventArgs e)
{
Configuration.ShowBalloonTooltips = ((ToolStripMenuItem)sender).Checked;
ChangedConfigurationContinuation.Schedule(TimeSpan.FromSeconds(1),
async () => { await SaveConfiguration(); }, _cancellationToken);
}
private void LaunchOnBootToolStripMenuItem_CheckedChanged(object sender, EventArgs e)
{
Configuration.LaunchOnBoot = ((ToolStripMenuItem)sender).Checked;
ChangedConfigurationContinuation.Schedule(TimeSpan.FromSeconds(1),
async () => { await SaveConfiguration(); }, _cancellationToken);
Miscellaneous.LaunchOnBootSet(Configuration.LaunchOnBoot);
}
private void AtomicOperationsToolStripMenuItem_CheckedChanged(object sender, EventArgs e)
{
Configuration.AtomicOperations = ((ToolStripMenuItem)sender).Checked;
ChangedConfigurationContinuation.Schedule(TimeSpan.FromSeconds(1),
async () => { await SaveConfiguration(); }, _cancellationToken);
}
private void TrashDatabaseToolStripMenuItem_Click(object sender, EventArgs e)
{
try
{
File.Delete(Constants.DatabaseFilePath);
if (Configuration.ShowBalloonTooltips)
{
ShowBalloon("Database Deleted", $"Database file {Constants.DatabaseFilePath} has been deleted.",
5000);
}
Log.Information($"Database file {Constants.DatabaseFilePath} has been deleted.");
}
catch (Exception exception)
{
if (Configuration.ShowBalloonTooltips)
{
ShowBalloon("Could not Delete Database",
$"Database file {Constants.DatabaseFilePath} delete failed with error: {exception.Message}",
5000);
}
Log.Information(
$"Database file {Constants.DatabaseFilePath} delete failed with error: {exception.Message}");
}
}
private void EnableToolStripMenuItem_CheckedChanged(object sender, EventArgs e)
{
Configuration.Enabled = enableToolStripMenuItem.Checked;
ChangedConfigurationContinuation.Schedule(TimeSpan.FromSeconds(1),
async () => { await SaveConfiguration(); }, _cancellationToken);
ToggleWatchers();
}
private void SnapshotsToolStripMenuItem_Click(object sender, EventArgs e)
{
if (_snapshotManagerForm != null)
{
return;
}
_snapshotManagerForm = new SnapshotManagerForm(this, _snapshotDatabase, _cancellationToken);
_snapshotManagerForm.Closing += SnapshotManagerFormClosing;
_snapshotManagerForm.Show();
}
private void SnapshotManagerFormClosing(object sender, CancelEventArgs e)
{
if (_snapshotManagerForm == null)
{
return;
}
_snapshotManagerForm.Closing -= SnapshotManagerFormClosing;
_snapshotManagerForm.Close();
_snapshotManagerForm = null;
}
private async void FileSystemWatcher_Changed(object sender, FileSystemEventArgs e)
{
// Ignore directories.
if (Directory.Exists(e.FullPath))
{
return;
}
await _changedFilesLock.WaitAsync(_cancellationToken);
try
{
var delay = global::TrackedFolders.Constants.Delay;
var color = Color.Empty;
if (TrackedFolders.TryGet(e.FullPath, out var folder))
{
delay = folder.Delay;
color = folder.Color;
}
if (_changedFiles.Contains(e.FullPath))
{
_changedFilesContinuation.Schedule(delay, async () => await TakeSnapshots(color, _cancellationToken), _cancellationToken);
return;
}
_changedFiles.Add(e.FullPath);
_changedFilesContinuation.Schedule(delay, async () => await TakeSnapshots(color, _cancellationToken), _cancellationToken);
}
catch (Exception exception)
{
Log.Error(exception, "Could not process changed files.");
}
finally
{
_changedFilesLock.Release();
}
}
private void AboutToolStripMenuItem_Click(object sender, EventArgs e)
{
if (_aboutForm != null)
{
return;
}
_aboutForm = new AboutForm(_cancellationToken);
_aboutForm.Closing += AboutForm_Closing;
_aboutForm.Show();
}
private void AboutForm_Closing(object sender, CancelEventArgs e)
{
if (_aboutForm == null)
{
return;
}
_aboutForm.Dispose();
_aboutForm = null;
}
private void QuitToolStripMenuItem_Click(object sender, EventArgs e)
{
Close();
}
private async void UpdateToolStripMenuItem_Click(object sender, EventArgs e)
{
// Manually check for updates, this will not show a ui
var result = await _sparkle.CheckForUpdatesQuietly();
if (result.Status == UpdateStatus.UpdateAvailable)
{
// if update(s) are found, then we have to trigger the UI to show it gracefully
_sparkle.ShowUpdateNeededUI();
return;
}
MessageBox.Show("No updates available at this time.", "Horizon", MessageBoxButtons.OK,
MessageBoxIcon.Asterisk,
MessageBoxDefaultButton.Button1, MessageBoxOptions.DefaultDesktopOnly, false);
}
private void NotifyIcon1_Click(object sender, EventArgs e)
{
if (e is MouseEventArgs mouseEventArgs && mouseEventArgs.Button == MouseButtons.Left)
{
}
}
private void ManageFoldersToolStripMenuItem_Click(object sender, EventArgs e)
{
if (_manageFoldersForm != null)
{
return;
}
_manageFoldersForm = new ManageFoldersForm(this, _cancellationToken);
_manageFoldersForm.Closing += ManageFoldersForm_Closing;
_manageFoldersForm.Show();
}
private void ManageFoldersForm_Closing(object sender, CancelEventArgs e)
{
if (_manageFoldersForm == null)
{
return;
}
_manageFoldersForm.Closing -= ManageFoldersForm_Closing;
_manageFoldersForm.Close();
_manageFoldersForm = null;
}
#endregion
#region Public Methods
public void ShowBalloon(string title, string text, int time)
{
notifyIcon1.BalloonTipTitle = title;
notifyIcon1.BalloonTipText = text;
notifyIcon1.ShowBalloonTip(time);
}
public async Task TakeSnapshot(string path)
{
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.TopDirectoryOnly))
{
try
{
var fileName = Path.GetFileName(file);
var directory = Path.GetDirectoryName(fileName);
var color = Color.Empty;
if (TrackedFolders.TryGet(directory, out var folder))
{
color = folder.Color;
}
await _snapshotDatabase.CreateSnapshotAsync(fileName, file, color, _cancellationToken);
}
catch (SQLiteException exception)
{
if (exception.ResultCode == SQLiteErrorCode.Constraint)
{
Log.Information(exception, "Snapshot already exists.");
}
}
catch (Exception exception)
{
Log.Error(exception, "Could not take snapshot.", file);
}
}
}
public async Task TakeSnapshotRecursive(string path)
{
foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
{
try
{
var fileName = Path.GetFileName(file);
var directory = Path.GetDirectoryName(fileName);
var color = Color.Empty;
if (TrackedFolders.TryGet(directory, out var folder))
{
color = folder.Color;
}
await _snapshotDatabase.CreateSnapshotAsync(fileName, file, color, _cancellationToken);
}
catch (SQLiteException exception)
{
if (exception.ResultCode == SQLiteErrorCode.Constraint)
{
Log.Information(exception, "Snapshot already exists.");
}
}
catch (Exception exception)
{
Log.Error(exception, "Could not take snapshot.", file);
}
}
}
public async Task SaveConfiguration()
{
if (!Directory.Exists(Constants.UserApplicationDirectory))
{
Directory.CreateDirectory(Constants.UserApplicationDirectory);
}
switch (await Serialization.Serialize(Configuration, Constants.ConfigurationFile, "Configuration",
"<!ATTLIST Configuration xmlns:xsi CDATA #IMPLIED xsi:noNamespaceSchemaLocation CDATA #IMPLIED>",
CancellationToken.None))
{
case SerializationSuccess<Configuration.Configuration> _:
Log.Information("Serialized configuration.");
break;
case SerializationFailure serializationFailure:
Log.Warning(serializationFailure.Exception.Message, "Failed to serialize configuration.");
break;
}
}
public static async Task<Configuration.Configuration> LoadConfiguration()
{
if (!Directory.Exists(Constants.UserApplicationDirectory))
{
Directory.CreateDirectory(Constants.UserApplicationDirectory);
}
var deserializationResult =
await Serialization.Deserialize<Configuration.Configuration>(Constants.ConfigurationFile,
Constants.ConfigurationNamespace, Constants.ConfigurationXsd, CancellationToken.None);
switch (deserializationResult)
{
case SerializationSuccess<Configuration.Configuration> serializationSuccess:
return serializationSuccess.Result;
case SerializationFailure serializationFailure:
Log.Warning(serializationFailure.Exception, "Failed to load configuration.");
return new Configuration.Configuration();
default:
return new Configuration.Configuration();
}
}
public static async Task<TrackedFolders.TrackedFolders> LoadFolders()
{
if (!Directory.Exists(Constants.UserApplicationDirectory))
{
Directory.CreateDirectory(Constants.UserApplicationDirectory);
}
var deserializationResult =
await Serialization.Deserialize<TrackedFolders.TrackedFolders>(Constants.FoldersFile,
Constants.TrackedFoldersNamespace, Constants.TrackedFoldersXsd, CancellationToken.None);
switch (deserializationResult)
{
case SerializationSuccess<TrackedFolders.TrackedFolders> serializationSuccess:
return serializationSuccess.Result;
case SerializationFailure serializationFailure:
Log.Warning(serializationFailure.Exception, "Failed to load tracked folders");
return new TrackedFolders.TrackedFolders();
default:
return new TrackedFolders.TrackedFolders();
}
}
public async Task SaveFolders()
{
if (!Directory.Exists(Constants.UserApplicationDirectory))
{
Directory.CreateDirectory(Constants.UserApplicationDirectory);
}
switch (await Serialization.Serialize(TrackedFolders, Constants.FoldersFile, "TrackedFolders",
"<!ATTLIST TrackedFolders xmlns:xsi CDATA #IMPLIED xsi:noNamespaceSchemaLocation CDATA #IMPLIED>",
CancellationToken.None))
{
case SerializationSuccess<TrackedFolders.TrackedFolders> _:
Log.Information("Serialized tracked folders.");
break;
case SerializationFailure serializationFailure:
Log.Warning(serializationFailure.Exception.Message, "Failed to serialize tracked folders.");
break;
}
}
#endregion
#region Private Methods
private void RemoveWatcher(string folder)
{
var removeList = new List<FileSystemWatcher>();
foreach (var fileSystemWatcher in FileSystemWatchers)
{
if (fileSystemWatcher.Path.IsPathEqual(folder) ||
fileSystemWatcher.Path.IsSubPathOf(folder))
{
removeList.Add(fileSystemWatcher);
}
}
foreach (var fileSystemWatcher in removeList)
{
FileSystemWatchers.Remove(fileSystemWatcher);
fileSystemWatcher.Changed -= FileSystemWatcher_Changed;
fileSystemWatcher.Dispose();
}
}
private void AddWatcher(string folder, bool recursive)
{
var fileSystemWatcher = new FileSystemWatcher
{
IncludeSubdirectories = recursive,
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Attributes,
Path = folder,
EnableRaisingEvents = true
};
fileSystemWatcher.Changed += FileSystemWatcher_Changed;
FileSystemWatchers.Add(fileSystemWatcher);
}
private void ToggleWatchers()
{
switch (Configuration.Enabled)
{
case true:
foreach (var watcher in FileSystemWatchers)
{
watcher.EnableRaisingEvents = true;
}
if (Configuration.ShowBalloonTooltips)
{
ShowBalloon("Watching", "Watching folders...", 5000);
}
Log.Information("Watching folders.");
break;
default:
foreach (var watcher in FileSystemWatchers)
{
watcher.EnableRaisingEvents = false;
}
if (Configuration.ShowBalloonTooltips)
{
ShowBalloon("Not Watching", "Folders are not being watched.", 5000);
}
Log.Information("Folders are not being watched.");
break;
}
}
private async Task TakeSnapshots(Color color, CancellationToken cancellationToken)
{
var bufferBlock = new BufferBlock<string>(new DataflowBlockOptions() {CancellationToken = cancellationToken});
var actionBlock = new ActionBlock<string>(async path =>
{
// In case files have vanished strictly due to the time specified by the tracked folders delay.
if (!File.Exists(path))
{
Log.Warning("File vanished after tracked folder delay.", path);
return;
}
try
{
var fileName = System.IO.Path.GetFileName(path);
var screenCapture = ScreenCapture.Capture((Utilities.CaptureMode)Configuration.CaptureMode);
await _snapshotDatabase.CreateSnapshotAsync(fileName, path, screenCapture, color,
_cancellationToken);
}
catch (SQLiteException exception)
{
if (exception.ResultCode == SQLiteErrorCode.Constraint)
{
Log.Information(exception, "Snapshot already exists.");
}
}
catch (Exception exception)
{
Log.Error(exception, "Could not take snapshot.", path);
}
});
using (var snapshotLink =
bufferBlock.LinkTo(actionBlock, new DataflowLinkOptions() { PropagateCompletion = true }))
{
await _changedFilesLock.WaitAsync(_cancellationToken);
try
{
foreach (var path in _changedFiles)
{
await bufferBlock.SendAsync(path, cancellationToken);
}
bufferBlock.Complete();
await bufferBlock.Completion;
}
catch (Exception exception)
{
Log.Error(exception, "Could not take snapshots.");
}
finally
{
_changedFiles.Clear();
_changedFilesLock.Release();
}
}
}
#endregion
}
}
Generated by GNU Enscript 1.6.5.90.