Skip to content
Snippets Groups Projects
AutoTxTray.cs 24.4 KiB
Newer Older
using System;
using System.Diagnostics;
using System.Timers;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using ATxCommon.Serializables;
using Microsoft.WindowsAPICodePack.Dialogs;
using NLog;
using NLog.Config;
using NLog.Targets;
using Timer = System.Timers.Timer;

// ReSharper disable RedundantDefaultMemberInitializer

namespace ATxTray
{
    public class AutoTxTray : ApplicationContext
    {
        private static readonly Logger Log = LogManager.GetCurrentClassLogger();

        // private static readonly string AppTitle = Path.GetFileNameWithoutExtension(Application.ExecutablePath);
        private const string AppTitle = "AutoTx Tray Monitor";

        private static readonly Timer AppTimer = new Timer(1000);
        private static string _statusFile;
        private static string _submitPath;
        private static ServiceConfig _config;
        private static ServiceStatus _status;

        private static bool _statusChanged = false;
        private static bool _statusFileChanged = true;
        private static bool _serviceProcessAlive = false;
        private static bool _serviceSuspended = true;
        private static string _serviceSuspendReason;

        private static bool _txInProgress = false;
        private static long _txSize;
        private static int _txProgressPct;


        #region tray icon and context menu variables

        private readonly NotifyIcon _notifyIcon = new NotifyIcon();
        private readonly Icon _tiDefault = Properties.Resources.IconDefault;
        private readonly Icon _tiStopped = Properties.Resources.IconStopped;
        private readonly Icon _tiSuspended = Properties.Resources.IconSuspended;
        private readonly Icon _tiTx0 = Properties.Resources.IconTx0;
        private readonly Icon _tiTx1 = Properties.Resources.IconTx1;
        private readonly ContextMenuStrip _cmStrip = new ContextMenuStrip();
        private readonly ToolStripMenuItem _miExit = new ToolStripMenuItem();
        private readonly ToolStripMenuItem _miTitle = new ToolStripMenuItem();
        private readonly ToolStripMenuItem _miSvcRunning = new ToolStripMenuItem();
        private readonly ToolStripMenuItem _miSvcSuspended = new ToolStripMenuItem();
        private readonly ToolStripMenuItem _miTxProgress = new ToolStripMenuItem();
        private readonly ToolStripMenuItem _miTxEnqueue = new ToolStripMenuItem();
        private readonly ToolStripProgressBar _miTxProgressBar = new ToolStripProgressBar();

        private static TaskDialog _confirmDialog;
        private static DirectoryInfo _selectedDir;
        /// <summary>
        /// Constructor setting up tray icon, config + status, timer and file system watcher.
        /// </summary>
        /// <param name="baseDir">The base directory of the AutoTx service installation.</param>
        public AutoTxTray(string baseDir) {

            SetupLogging();
            _statusFile = Path.Combine(baseDir, "var", "status.xml");
            Log.Info("-----------------------");
            Log.Info("{0} initializing...", AppTitle);
            Log.Info("build: [{0}]", Properties.Resources.BuildDate.Trim());
            Log.Info("commit: [{0}]", Properties.Resources.BuildCommit.Trim());
            Log.Info("-----------------------");
            Log.Debug(" - status file: [{0}]", _statusFile);

            _notifyIcon.Icon = _tiStopped;
            _notifyIcon.Visible = true;
            _notifyIcon.DoubleClick += PickDirectoryForNewTransfer;
            // this doesn't work properly, the menu will not close etc. so we disable it for now:
            // _notifyIcon.Click += ShowContextMenu;
            Log.Trace("Trying to read service config and status files...");
                _config = ServiceConfig.Deserialize(Path.Combine(baseDir, "conf"));
                _submitPath = Path.Combine(_config.IncomingPath, Environment.UserName);
                UpdateStatusInformation();

                var fsw = new FileSystemWatcher {
                    Path = Path.Combine(baseDir, "var"),
                    NotifyFilter = NotifyFilters.LastWrite,
                    Filter = "status.xml",
                };
                fsw.Changed += StatusFileUpdated;
                fsw.EnableRaisingEvents = true;

                Log.Info("{0} initialization completed.", AppTitle);
                var msg = "Error during initialization: " + ex.Message;
                Log.Error(msg);
                _notifyIcon.ShowBalloonTip(5000, AppTitle, msg, ToolTipIcon.Error);
                // we cannot terminate the message loop (Application.Run()) while the constructor
                // is being run as it is not active yet - therefore we set the _status object to
                // null which will terminate the application during the next "Elapsed" event:
                _status = null;
                // suspend the thread for 5s to make sure the balloon tip is shown for a while:
                System.Threading.Thread.Sleep(5000);
            // we need to enable the timer no matter whether the initialization steps above have
            // succeeded since this is the only way to cleanly exit the application (by checking
            // the _status in the AppTimerElapsed method):
            AppTimer.Elapsed += AppTimerElapsed;
            AppTimer.Enabled = true;
            Log.Trace("Enabled timer.");
        }

        /// <summary>
        /// Configure logging using a file target.
        /// </summary>
        private static void SetupLogging() {
            var logConfig = new LoggingConfiguration();
            var fileTarget = new FileTarget {
                FileName = $"var/{Path.GetFileNameWithoutExtension(Application.ExecutablePath)}.log",
                Layout = @"${date:format=yyyy-MM-dd HH\:mm\:ss} [${level}] ${message}"
                // Layout = @"${date:format=yyyy-MM-dd HH\:mm\:ss} [${level}] (${logger}) ${message}"
            };
            logConfig.AddTarget("file", fileTarget);
            var logRule = new LoggingRule("*", LogLevel.Debug, fileTarget);
            logConfig.LoggingRules.Add(logRule);
            LogManager.Configuration = logConfig;
        /// <summary>
        /// Set up the tray icon context menu entries.
        /// </summary>
        private void SetupContextMenu() {
            Log.Trace("Building context menu...");
            _miTitle.Font = new Font(_cmStrip.Font, FontStyle.Bold);
            _miTitle.Text = AppTitle;
            _miTitle.ToolTipText = Properties.Resources.BuildCommit.Trim();
            _miTitle.Image = _tiDefault.ToBitmap();
            _miTitle.BackColor = Color.LightCoral;
            _miTitle.Click += ShowContextMenu;
            _miSvcRunning.Text = @"Service NOT RUNNING!";
            _miSvcRunning.BackColor = Color.LightCoral;
            _miSvcRunning.Click += ShowContextMenu;

            _miSvcSuspended.Text = @"No limits apply, service active.";
            _miSvcSuspended.Click += ShowContextMenu;
            _miTxProgress.Text = @"No transfer running.";
            _miTxProgress.Click += ShowContextMenu;

            _miTxEnqueue.Text = @"+++ Add new directory for transfer. +++";
            _miTxEnqueue.Click += PickDirectoryForNewTransfer;
            _miTxProgressBar.ToolTipText = @"Current Transfer Progress";
            _miTxProgressBar.Value = 0;
            var size = _miTxProgressBar.Size;
            size.Width = 300;
            _miTxProgressBar.Size = size;

            _cmStrip.Items.AddRange(new ToolStripItem[] {
                _miTitle,
                _miSvcRunning,
                _miSvcSuspended,
                _miTxProgressBar,
                new ToolStripSeparator(),
            });

            _notifyIcon.ContextMenuStrip = _cmStrip;
            Log.Trace("Finished building context menu.");

        /// <summary>
        /// Clean up the tray icon and shut down the application.
        /// </summary>
        private void AutoTxTrayExit() {
            _notifyIcon.Visible = false;
Niko Ehrenfeuchter's avatar
Niko Ehrenfeuchter committed
            Log.Info("Shutting down {0}.", AppTitle);
        /// <summary>
        /// Wrapper for AutoTxTrayExit to act as an event handler.
        /// </summary>
        private void AutoTxTrayExit(object sender, EventArgs e) {
            AutoTxTrayExit();
        }

        /// <summary>
        /// Update the tooltip making sure not to exceed the 63 characters limit.
        /// </summary>
        /// <param name="msg"></param>
        private void UpdateHoverText(string msg) {
            if (msg.Length > 63) {
                msg = msg.Substring(0, 60) + "...";
            }
            _notifyIcon.Text = msg;
        }

        /// <summary>
        /// Refresh status information and update tray icon and context menu items accordingly.
        /// </summary>
        private void AppTimerElapsed(object sender, ElapsedEventArgs e) {
            if (_status == null) {
            UpdateServiceProcessState();
            UpdateStatusInformation();  // update the status no matter if the service process is running
            var svcProcessRunning = "stopped";
            var statusHeartbeat = "?";
Niko Ehrenfeuchter's avatar
Niko Ehrenfeuchter committed
            var txProgress = "No";
            if (_serviceProcessAlive) {
                svcProcessRunning = "OK";
Niko Ehrenfeuchter's avatar
Niko Ehrenfeuchter committed
                if ((DateTime.Now - _status.LastStatusUpdate).TotalSeconds <= 60)
                    statusHeartbeat = "OK";
                    txProgress = $"{_txProgressPct}%";
            UpdateHoverText($"AutoTx [svc={svcProcessRunning}] [hb={statusHeartbeat}] [tx={txProgress}]");
            if (!_statusChanged)
                return;

            UpdateServiceSuspendedState();
            UpdateTxProgressBar();
            UpdateTxInProgressState();

            UpdateTrayIcon();
            _statusChanged = false;
        /// <summary>
        /// Set global flag indicating the status file has changed and needs to be re-read.
        /// </summary>
        private static void StatusFileUpdated(object sender, FileSystemEventArgs e) {
        /// <summary>
        /// Event handler to make the context menu appear on the screen.
        /// </summary>
        private void ShowContextMenu(object sender, EventArgs e) {
            // just show the menu again, to avoid that clicking the menu item closes the context
            // menu without having to disable the item (which would grey out the text and icon):
            _notifyIcon.ContextMenuStrip.Show();
        }

        /// <summary>
        /// Let the user select a directory for starting a new transfer.
        /// </summary>
        private static void PickDirectoryForNewTransfer(object sender, EventArgs e) {
            if (!Directory.Exists(_submitPath)) {
                Log.Error("Current user has no incoming directory: [{0}]", _submitPath);
                MessageBox.Show($@"User '{Environment.UserName}' is not allowed to start transfers!",
                    @"User not registered for AutoTx", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }

            var dirDialog = new CommonOpenFileDialog {
                Title = @"Select directory to be transferred",
                IsFolderPicker = true,
                EnsurePathExists = true,

            if (dirDialog.ShowDialog() != CommonFileDialogResult.Ok)
                return;

            _selectedDir = new DirectoryInfo(dirDialog.FileName);
            Log.Debug($"Selected path from folder picker: [{_selectedDir.Name}]");
            var drive = dirDialog.FileName.Substring(0, 3);
            if (drive != _config.SourceDrive) {
                MessageBox.Show($@"The selected directory '{_selectedDir}' is required to be on " +
                    $@"drive {_config.SourceDrive}, please choose another directory!",
                    @"Selected directory on wrong drive", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }

            if (_selectedDir.Name.Length <= 3) {
                MessageBox.Show($"Submitting entire drives ({_selectedDir.Name}) is not allowed!",
                    "Invalid selection", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }

            try {
                NewTxConfirmationDialog();
            }
            catch (UnauthorizedAccessException ex) {
                MessageBox.Show("ERROR: the selected directory\n\n" +
                                $"[{_selectedDir.Name}]\n\n" +
                                "contains files or folders that are not readable\n" +
                                "due to insufficient permissions!\n\n" +
                                ex.Message,
                    "Error reading directory", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        /// <summary>
        /// Let the user confirm the directory choice by presenting a summary with name, size etc.
        /// </summary>
        private static void NewTxConfirmationDialog() {
            var folderName = _selectedDir.Name;
            var locationPath = _selectedDir.Parent?.FullName;
            var size = Conv.BytesToString(FsUtils.GetDirectorySize(_selectedDir.FullName));

            const string caption = "AutoTx - Folder Selection Confirmation";
            const string instructionText = "Review your folder selection:";
            var footerText = "Selection summary:\n\n" +
                             $"Selected Folder:  [{folderName}]\n" +
                             $"Size:  {size}\n" +
                             $"Folder Location:  [{locationPath}]";

            _confirmDialog = new TaskDialog {
                // Icon is buggy in the API and has to be set via an event handler, see below
                // Icon = TaskDialogStandardIcon.Shield,
                InstructionText = instructionText,
                FooterText = footerText,
                DetailsExpanded = true,
                StandardButtons = TaskDialogStandardButtons.Cancel,
            };
            // register the event handler to set the icon:
            _confirmDialog.Opened += TaskDialogOpened;

            var acceptBtn = new TaskDialogCommandLink("buttonAccept",
                $"Accept \"{folderName}\" with a total size of {size}.",
                $"Transfer \"{folderName}\" from \"{locationPath}\".");
            var changeBtn = new TaskDialogCommandLink("buttonCancel", "Select different folder...",
                "Do not use this folder, select another one instead.");
            acceptBtn.Click += ConfirmAcceptClick;
            changeBtn.Click += ConfirmChangeClick;

            _confirmDialog.Controls.Add(acceptBtn);
            _confirmDialog.Controls.Add(changeBtn);
            try {
                _confirmDialog.Show();
            }
            catch (Exception ex) {
                Log.Error("Showing the TaskDialog failed: {0}", ex.Message);
                var res = MessageBox.Show($@"{instructionText}\n{footerText}\n\n" +
                                @"Press [OK] to confirm selection.", caption,
                    MessageBoxButtons.OKCancel, MessageBoxIcon.Question);
                if (res == DialogResult.OK)
                    SubmitDirForNewTx();
        /// <summary>
        /// Dummy handler to set the TaskDialog icon.
        /// </summary>
        private static void TaskDialogOpened(object sender, EventArgs e) {
            var td = sender as TaskDialog;
            td.Icon = TaskDialogStandardIcon.Shield;
        }

        /// <summary>
        /// Close the confirmation dialog and submit the selected dir for transfer.
        /// </summary>
        private static void ConfirmAcceptClick(object sender, EventArgs e) {
            _confirmDialog.Close();
            SubmitDirForNewTx();
        /// <summary>
        /// Close the confirmation dialog and re-show the directory picker.
        /// </summary>
        private static void ConfirmChangeClick(object sender, EventArgs e) {
            _confirmDialog.Close();
            Log.Debug("User wants to change directory choice.");
            PickDirectoryForNewTransfer(sender, e);
        /// <summary>
        /// Submit the selected directory as a new transfer.
        /// 
        /// The chosen folder will be moved to the AutoTx "incoming" location of the current user
        /// where it will be picked up by the service as a new transfer.
        /// </summary>
        private static void SubmitDirForNewTx() {
            Log.Debug($"User accepted directory choice [{_selectedDir.FullName}].");
            var tgtPath = Path.Combine(_submitPath, _selectedDir.Name);
            try {
                Directory.Move(_selectedDir.FullName, tgtPath);
            catch (Exception ex) {
                Log.Error("Moving [{0}] to [{1}] failed: {2}", _selectedDir.FullName, tgtPath, ex);
                MessageBox.Show($@"Error submitting {_selectedDir.FullName} for transfer: {ex}",
                    @"AutoTx New Transfer Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }

            Log.Info($"Submitted new transfer: [{_selectedDir.FullName}].");
        /// <summary>
        /// Read (or re-read) the service status file if it has changed since last time.
        /// </summary>
        private static void UpdateStatusInformation() {
Niko Ehrenfeuchter's avatar
Niko Ehrenfeuchter committed
            Log.Trace("Status file was updated, trying to re-read...");
            _status = ServiceStatus.Deserialize(_statusFile, _config);
            _statusChanged = true;
        }

        /// <summary>
        /// Check if a process with the expeced name of the service is currently running.
        /// </summary>
        /// <returns>True if such a process exists, false otherwise.</returns>
        private static bool IsServiceProcessAlive() {
            var plist = Process.GetProcessesByName("AutoTx");
            return plist.Length > 0;
        }

        /// <summary>
        /// Check if the service process is alive and update context menu entries accordingly.
        /// </summary>
        private void UpdateServiceProcessState() {
            var isServiceProcessAlive = IsServiceProcessAlive();
            if (_serviceProcessAlive == isServiceProcessAlive)
            _serviceProcessAlive = isServiceProcessAlive;
            if (_serviceProcessAlive) {
                _miSvcRunning.Text = @"Service running.";
                _miSvcRunning.BackColor = Color.LightGreen;
                _miTitle.BackColor = Color.LightGreen;
                _miSvcSuspended.Enabled = true;
                _notifyIcon.ShowBalloonTip(500, AppTitle,
                    "Service running.", ToolTipIcon.Info);
                _miSvcRunning.Text = @"Service NOT RUNNING!";
                _miSvcRunning.BackColor = Color.LightCoral;
                _miTitle.BackColor = Color.LightCoral;
                _miSvcSuspended.Enabled = false;
                _notifyIcon.ShowBalloonTip(500, AppTitle,
                    "Service stopped.", ToolTipIcon.Error);
        /// <summary>
        /// Update the context menu with the current "suspended" state of the service.
        /// </summary>
        private void UpdateServiceSuspendedState() {
            // first update the suspend reason as this can possibly change even if the service
            // never leaves the suspended state and we should still display the correct reason:
            if (_serviceSuspendReason == _status.StatusDescription &&
                _serviceSuspended == _status.ServiceSuspended)
            _serviceSuspended = _status.ServiceSuspended;
            _serviceSuspendReason = _status.StatusDescription;
            if (_serviceSuspended) {
                _miSvcSuspended.Text = @"Service suspended, reason: " + _serviceSuspendReason;
                _miSvcSuspended.BackColor = Color.LightSalmon;
                _notifyIcon.ShowBalloonTip(500, "AutoTx Monitor",
                    "Service suspended: " + _status.StatusDescription, ToolTipIcon.Warning);
            } else {
                _miSvcSuspended.Text = @"No limits apply, service active.";
                _miSvcSuspended.BackColor = Color.LightGreen;
                _notifyIcon.ShowBalloonTip(500, "AutoTx Monitor",
                    "Service resumed, no limits apply.", ToolTipIcon.Info);
        /// <summary>
        /// Update the context menu regarding the current transfer state, show a balloon tooltip
        /// if the transfer status has changed.
        /// </summary>
        private void UpdateTxInProgressState() {
            if (_txInProgress == _status.TransferInProgress &&
                _txSize == _status.CurrentTransferSize)
                return;

            _txInProgress = _status.TransferInProgress;
            _txSize = _status.CurrentTransferSize;
            if (_txInProgress) {
                _miTxProgress.Text = $@"Transfer in progress (size: {Conv.BytesToString(_txSize)})";
                _miTxProgress.BackColor = Color.LightGreen;
                _notifyIcon.ShowBalloonTip(500, AppTitle,
                    "New transfer started (size: " +
                    Conv.BytesToString(_txSize) + ").", ToolTipIcon.Info);
            } else {
                _miTxProgress.Text = @"No transfer running.";
                _miTxProgress.ResetBackColor();
                _notifyIcon.ShowBalloonTip(500, AppTitle,
                    "Transfer completed.", ToolTipIcon.Info);
        /// <summary>
        /// Update the transfer progress bar.
        /// </summary>
        private void UpdateTxProgressBar() {
            if (_txInProgress == _status.TransferInProgress &&
                _txProgressPct == _status.CurrentTransferPercent)
                return;

            _txProgressPct = _status.CurrentTransferPercent;
            if (_txInProgress) {
                Log.Debug("Transfer progress: {0}%", _txProgressPct);
                _miTxProgressBar.Visible = true;
                _miTxProgressBar.Value = _txProgressPct;
                _miTxProgressBar.ToolTipText = _txProgressPct.ToString();
            } else {
                _miTxProgressBar.Value = 0;
                _miTxProgressBar.Visible = false;
                _miTxProgressBar.ToolTipText = @"Current Transfer Progress";
        /// <summary>
        /// Update the tray icon reflecting the current service and transfer status.
        /// </summary>
        private void UpdateTrayIcon() {
            // if a transfer is running and active show the transfer icon, alternating between its
            // two variants every second ("blinking")
            // NOTE: this is independent of a status change as the blinking should still happen
            // even if the status (file) has not been updated in between
            if (_txInProgress && !_serviceSuspended) {
                if (DateTime.Now.Second % 2 == 0) {
                    _notifyIcon.Icon = _tiTx0;
                } else {
                    _notifyIcon.Icon = _tiTx1;
                }
            }

            // now we can check if a status change occurred and just return otherwise:
            // show the "stopped" icon if the service process is not running:
            if (!_serviceProcessAlive) {
                _notifyIcon.Icon = _tiStopped;
                return;
            }

            // show the "suspended" icon of the service is in the corresponding state:
            if (_serviceSuspended) {
                _notifyIcon.Icon = _tiSuspended;
                return;
            }

            // if none of the above is true and no transfer is running show the default icon:
            if (!_txInProgress) {
                _notifyIcon.Icon = _tiDefault;
            }
        }