csharp/AutoItConsulting/osd-background/src/AutoIt.OSD.Background/FormBackground.cs

FormBackground.cs
//  
// Copyright (c) AutoIt Consulting Ltd. All rights reserved.  
// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.  
// using System;
//

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.IO.Pipes;
using System.Linq;
using System.Reflection;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Threading;
using System.Windows.Forms;
using System.Xml.Serialization;
using AutoIt.OSD.Background.Properties;
using AutoIt.Windows;
using Microsoft.Win32;

namespace AutoIt.OSD.Background
{
    public partial clast FormBackground : Form
    {
        private const string MutexName = "MUTEX_AUTOIT_OSD_BACKGROUND";
        private const string PipeName = "PIPE_AUTOIT_OSD_BACKGROUND";

        private const int TimerIntervalSecs = 1;

        /// 
        ///     Directory containing this instance of the exe
        /// 
        private readonly string _appPath = Directory.GetParent(astembly.GetExecutingastembly().Location).ToString();

        /// 
        ///     Signal to update the options recieved from another instance.
        /// 
        private readonly ManualResetEvent _eventNewOptionsAvailable = new ManualResetEvent(false);

        /// 
        ///     Signal for the application to close.
        /// 
        private readonly ManualResetEvent _eventShutdownRequested = new ManualResetEvent(false);

        private readonly KeyboardHook _keyboardHook = new KeyboardHook();
        private readonly object _namedPiperServerThreadLock = new object();
        private bool _customBackgroundEnabled;

        private bool _firstApplicationInstance;

        private FormTools _formTools;

        private Mutex _mutexApplication;

        private IAsyncResult _namedPipeAsyncResult;
        private NamedPipeServerStream _namedPipeServerStream;
        private Thread _namedPipeServerThread;
        private NamedPipeXmlPayload _namedPipeXmlPayload;

        private Options _options;

        private string _osdBackgroundDir;

        private Color _progressBarBackColor;
        private DockStyle _progressBarDock;
        private bool _progressBarEnabled;
        private Color _progressBarForeColor;
        private int _progressBarHeight;
        private int _progressBarOffset;

        private bool _startedInTaskSequence;
        private bool _taskSequenceVariablesEnabled;
        private bool _userToolsEnabled;

        private DateTime _wallpaperLastModified;
        private string _wallpaperPath = string.Empty;

        /// 
        public FormBackground()
        {
            InitializeComponent();

            // Set main icon
            Icon = Resources.main;

            // Set these here rather than designer as it makes it easier to work with designer
            pictureBoxBackground.Dock = DockStyle.Fill;
            pictureBoxBackground.SizeMode = PictureBoxSizeMode.StretchImage;

            // Store the exe dir and working dir so we can past this info to another instance
            _osdBackgroundDir = Directory.GetParent(astembly.GetExecutingastembly().Location).ToString();
        }

        /// 
        protected override bool ShowWithoutActivation
        {
            get
            {
                return true;
            }
        }

        /// 
        ///     Gets a value indicating if the tools menu is currently showing.
        /// 
        private bool ShowingPastwordOrTools { get; set; }

        /// 
        ///     Check if this is Windows 8 or later. Requires an OS manifest to work correctly.
        /// 
        /// 
        public static bool IsWin8OrLater()
        {
            //    Windows 10	10.0*
            //    Windows Server 2016	10.0*
            //    Windows 8.1	6.3*
            //    Windows Server 2012 R2	6.3*
            //    Windows 8	6.2
            //    Windows Server 2012	6.2
            //    Windows 7	6.1

            if (Environment.OSVersion.Version.Major < 6)
            {
                return false;
            }

            if (Environment.OSVersion.Version.Major > 6)
            {
                return true;
            }

            // Major = 6
            return Environment.OSVersion.Version.Minor >= 2;
        }

        /// 
        ///     Returns path of the current user wallpaper.
        /// 
        /// 
        private static string GetCurrentUserWallpaperPath()
        {
            string wallpaperPath = string.Empty;

            try
            {
                RegistryKey userWallpaper = Registry.CurrentUser.OpenSubKey("Control Panel\\Desktop", false);
                if (userWallpaper != null)
                {
                    wallpaperPath = userWallpaper.GetValue("Wallpaper").ToString();
                }
            }
            catch (Exception)
            {
                // ignored
            }

            return wallpaperPath;
        }

        /// 
        ///     Look for an existing process with this name and terminate it.
        /// 
        private static void KillPreviousInstance()
        {
            Process[] processes = Process.GetProcessesByName(AppDomain.CurrentDomain.FriendlyName.Remove(AppDomain.CurrentDomain.FriendlyName.Length - 4));
            if (processes.Length = 2)
            {
                string arg = arguments[1].ToUpper();

                if (arg == "/?" || arg == "?")
                {
                    var usage = @"AutoIt.OSD.Background [/Close] | [Options.xml]";
                    MessageBox.Show(usage, Text, MessageBoxButtons.OK, MessageBoxIcon.Information);
                    return false;
                }

                if (arg == "/CLOSE" || arg == "CLOSE")
                {
                    // If we are not the first instance, send a quit message along the pipe
                    if (!IsApplicationFirstInstance())
                    {
                        //KillPreviousInstance();
                        var namedPipeXmlPayload = new NamedPipeXmlPayload
                        {
                            SignalQuit = true
                        };

                        NamedPipeClientSendOptions(namedPipeXmlPayload);
                    }

                    return false;
                }

                // Get the options filename
                optionsFilename = arguments[1];
            }

            // If no filename specified, use options.xml in current folder
            if (arguments.Length == 1)
            {
                optionsFilename = _appPath + @"\Options.xml";
            }

            try
            {
                var deSerializer = new XmlSerializer(typeof(Options));
                TextReader reader = new StreamReader(optionsFilename);
                _options = (Options)deSerializer.Deserialize(reader);
                _options.SanityCheck();
            }
            catch (Exception e)
            {
                string message = Resources.UnableToParseXml;

                if (e.InnerException != null)
                {
                    message += "\n\n" + e.InnerException.Message;
                }

                MessageBox.Show(message, Text, MessageBoxButtons.OK, MessageBoxIcon.Error);
                return false;
            }

            // Additional parsing of some string values in useful fields
            if (!OptionsXmlOptionsObjectToFields(_options))
            {
                return false;
            }

            // If are not the first instance then send a message to the running app with the new options and then quit
            if (!IsApplicationFirstInstance())
            {
                var namedPipeXmlPayload = new NamedPipeXmlPayload
                {
                    SignalQuit = false,
                    OsdBackgroundDir = _osdBackgroundDir,
                    OsdBackgroundWorkingDir = Directory.GetCurrentDirectory(),
                    Options = _options
                };

                NamedPipeClientSendOptions(namedPipeXmlPayload);
                return false;
            }

            return true;
        }

        /// 
        ///     Some of the strings in the xml object need parsing, this populates fields for those values
        /// 
        /// Returns true if all fields successfully parsed and valid.
        private bool OptionsXmlOptionsObjectToFields(Options xmlOptions)
        {
            // TODO: Move this logic into Options.cs

            try
            {
                _customBackgroundEnabled = xmlOptions.CustomBackground.Enabled;
                _userToolsEnabled = xmlOptions.UserTools.EnabledAdmin | xmlOptions.UserTools.EnabledUser;
                _taskSequenceVariablesEnabled = xmlOptions.TaskSequenceVariables.EnabledAdmin || xmlOptions.TaskSequenceVariables.EnabledUser;

                _progressBarEnabled = xmlOptions.CustomBackground.ProgressBar.Enabled;
                _progressBarHeight = xmlOptions.CustomBackground.ProgressBar.Height;
                _progressBarOffset = xmlOptions.CustomBackground.ProgressBar.Offset;
                _progressBarDock = (DockStyle)Enum.Parse(typeof(DockStyle), xmlOptions.CustomBackground.ProgressBar.Dock, true);
                _progressBarForeColor = ColorTranslator.FromHtml(xmlOptions.CustomBackground.ProgressBar.ForeColor);
                _progressBarBackColor = ColorTranslator.FromHtml(xmlOptions.CustomBackground.ProgressBar.BackColor);

                if (_progressBarDock != DockStyle.Bottom && _progressBarDock != DockStyle.Top)
                {
                    _progressBarDock = DockStyle.Bottom;
                }
            }
            catch (Exception)
            {
                return false;
            }

            return true;
        }

        /// 
        ///     Fully redraws the progress bar, also handles size changes based on the screen size.
        /// 
        private void ProgressBarRedraw()
        {
            if (!_progressBarEnabled)
            {
                return;
            }

            // Format to client size
            progressBar.Left = ClientRectangle.Left;
            progressBar.Width = ClientSize.Width;
            progressBar.Height = _progressBarHeight;

            if (_progressBarDock == DockStyle.Bottom)
            {
                progressBar.Top = ClientSize.Height - _progressBarHeight - _progressBarOffset;
            }
            else if (_progressBarDock == DockStyle.Top)
            {
                progressBar.Top = _progressBarOffset;
            }

            if (progressBar.Top < ClientRectangle.Top)
            {
                progressBar.Top = ClientRectangle.Top;
            }
            else if (progressBar.Top > ClientRectangle.Bottom)
            {
                progressBar.Top = ClientRectangle.Bottom;
            }

            progressBar.ForeColor = _progressBarForeColor;
            progressBar.BackColor = _progressBarBackColor;

            // Ensure in front of picture box
            progressBar.BringToFront();
        }

        /// 
        ///     Updates the progress bar based on the position in the task sequence.
        /// 
        private void ProgressBarRefresh()
        {
            // Get position in task sequence if there is one
            int currentInstruction;
            int lastInstruction;

            try
            {
#if DEBUG
                currentInstruction = 50;
                lastInstruction = 100;
#else

// Get the current position in the task sequence - will get blanks and exceptions if not in a TS
                currentInstruction = int.Parse(TaskSequence.GetVariable("_SMSTSNextInstructionPointer")) + 1;
                lastInstruction = int.Parse(TaskSequence.GetVariable("_SMSTSInstructionTableSize")) + 1;
#endif

                if (currentInstruction > lastInstruction)
                {
                    currentInstruction = lastInstruction;
                }
            }
            catch (Exception)
            {
                // Error reading task sequence variables, remove progress bar
                progressBar.Visible = false;

                // Have we been running in a task sequence before? If so, astume that the task sequence has ended
                // and close down - this prevents situations where the caller forgets to close us at the end of a TS
                if (_startedInTaskSequence)
                {
                    _eventShutdownRequested.Set();
                }

                return;
            }

            // If we reached here, we are in a task sequence, update flag
            _startedInTaskSequence = true;

            // If bar is not enabled then nothing else to do
            if (!_progressBarEnabled)
            {
                progressBar.Visible = false;
                return;
            }

            // Set percentage and make visible
            progressBar.Value = 100 * currentInstruction / lastInstruction;
            progressBar.Visible = true;
        }

        /// 
        ///     Refresh the picture box with the current user wallpaper
        /// 
        /// 
        /// 
        private bool RefreshBackgroundImage(bool forceUpdate = false)
        {
            // Get the current user's wallpaper
            string wallpaperPath = GetCurrentUserWallpaperPath();

            // If wallpaper is blank or doesn't exist then quit as we can't do anything useful
            if (string.IsNullOrEmpty(wallpaperPath) || !File.Exists(wallpaperPath))
            {
                return false;
            }

            // Get Current filename and filetime and check if we need to update the image
            DateTime modifiedTime = File.GetLastWriteTime(wallpaperPath);
            if (!forceUpdate && _wallpaperPath == wallpaperPath && _wallpaperLastModified.CompareTo(modifiedTime) == 0)
            {
                return false;
            }

            // Save new values
            _wallpaperPath = wallpaperPath;
            _wallpaperLastModified = modifiedTime;

            // Set the form bitmap and force display to primary monitor
            StartPosition = FormStartPosition.Manual;
            Location = Screen.PrimaryScreen.Bounds.Location;

            WindowState = FormWindowState.Maximized;

            // Don't use Maximized as it goes over the task bar which can be ugly
            //Size = new Size(Screen.GetWorkingArea(this).Width, Screen.GetWorkingArea(this).Height);

            try
            {
                using (var fileStream = new FileStream(wallpaperPath, FileMode.Open, FileAccess.Read))
                {
                    pictureBoxBackground.BackgroundImage = Image.FromStream(fileStream);
                    pictureBoxBackground.Invalidate();
                }
            }
            catch (Exception)
            {
                return false;
            }

            return true;
        }

        /// 
        ///     Event to handle display settings changes.
        /// 
        /// 
        /// 
        private void SystemEvents_DisplaySettingsChanged(object sender, EventArgs e)
        {
            // Force update of background image in case of resolution change
            RefreshBackgroundImage(true);

            // Push the Win7/Win10 progress screen to the bottom, then put our screen on top
            BringToFrontOfWindowsSetupProgress();

            // Resize progress bar
            ProgressBarRedraw();
        }

        /// 
        ///     Closes form when detects logoff/shutdown.
        /// 
        /// 
        /// 
        private void SystemEvents_SessionEnding(object sender, SessionEndingEventArgs e)
        {
            Close();
        }

        /// 
        ///     This will perform the neccessary changes based on an options change request. This is handled during a timer request
        ///     and the timer will be paused.
        /// 
        private void TimerHandleOptionsChangeEvent()
        {
            // Lock so no chance of receiving another new options file during processing
            lock (_namedPiperServerThreadLock)
            {
                if (_eventNewOptionsAvailable.WaitOne(0))
                {
                    _eventNewOptionsAvailable.Reset();

                    // Save data
                    _osdBackgroundDir = _namedPipeXmlPayload.OsdBackgroundDir;
                    _options = _namedPipeXmlPayload.Options;

                    // Change fields based on updated options data
                    OptionsXmlOptionsObjectToFields(_options);

                    // Change the working dir to match the last AutoIt.OSDBackground.exe that was run
                    Directory.SetCurrentDirectory(_namedPipeXmlPayload.OsdBackgroundWorkingDir);

                    // Redraw the background and progress bar (size and/or color might have new options)
                    RefreshBackgroundImage(true);
                    ProgressBarRedraw();
                }
            }
        }

        /// 
        ///     Called on interval to update bitmap and variables and check for quit signals
        /// 
        /// 
        /// 
        private void TimerHandleTickEvent(object sender, EventArgs e)
        {
            // If the tools menu is showing then don't do anything (ignores quit signals, new options until tools closed)
            if (ShowingPastwordOrTools)
            {
                return;
            }

            // Stop time while we process
            timerRefresh.Stop();

            // Has a new options file been received?  If so handle it.
            TimerHandleOptionsChangeEvent();

            // Update the background image if it's changed
            RefreshBackgroundImage();

            // Update overall progress bar percentage
            ProgressBarRefresh();

            // Is quit signalled? We only check it when tools not showing to prevent another instance loading
            // and closing our app while using the tools menu
            if (_eventShutdownRequested.WaitOne(0))
            {
                // Close form, and don't restart timer
                Close();
            }

            // Restart timer
            timerRefresh.Start();
        }
    }
}