Horizon – Rev 11

Subversion Repositories:
Rev:
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.