diff --git a/SafeExamBrowser.Contracts/I18n/Key.cs b/SafeExamBrowser.Contracts/I18n/Key.cs index 832dfe5e..16c021fc 100644 --- a/SafeExamBrowser.Contracts/I18n/Key.cs +++ b/SafeExamBrowser.Contracts/I18n/Key.cs @@ -21,7 +21,15 @@ namespace SafeExamBrowser.Contracts.I18n MessageBox_StartupErrorTitle, Notification_AboutTooltip, SplashScreen_InitializeBrowser, + SplashScreen_InitializeProcessMonitoring, + SplashScreen_InitializeTaskbar, + SplashScreen_InitializeWorkArea, + SplashScreen_RestoreWorkArea, + SplashScreen_ShutdownProcedure, SplashScreen_StartupProcedure, + SplashScreen_StopProcessMonitoring, + SplashScreen_WaitExplorerStartup, + SplashScreen_WaitExplorerTermination, Version } } diff --git a/SafeExamBrowser.Contracts/Monitoring/IKeyboardInterceptor.cs b/SafeExamBrowser.Contracts/Monitoring/IKeyboardInterceptor.cs new file mode 100644 index 00000000..77f717ca --- /dev/null +++ b/SafeExamBrowser.Contracts/Monitoring/IKeyboardInterceptor.cs @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2017 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +namespace SafeExamBrowser.Contracts.Monitoring +{ + public interface IKeyboardInterceptor + { + } +} diff --git a/SafeExamBrowser.Contracts/Monitoring/IMouseInterceptor.cs b/SafeExamBrowser.Contracts/Monitoring/IMouseInterceptor.cs new file mode 100644 index 00000000..a69d7f2e --- /dev/null +++ b/SafeExamBrowser.Contracts/Monitoring/IMouseInterceptor.cs @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2017 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +namespace SafeExamBrowser.Contracts.Monitoring +{ + public interface IMouseInterceptor + { + } +} diff --git a/SafeExamBrowser.Contracts/Monitoring/IProcessMonitor.cs b/SafeExamBrowser.Contracts/Monitoring/IProcessMonitor.cs new file mode 100644 index 00000000..7b12ba0c --- /dev/null +++ b/SafeExamBrowser.Contracts/Monitoring/IProcessMonitor.cs @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2017 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +namespace SafeExamBrowser.Contracts.Monitoring +{ + public interface IProcessMonitor + { + /// + /// Starts a new instance of the Windows explorer shell. + /// + void StartExplorerShell(); + + /// + /// Starts monitoring the Windows explorer, i.e. any newly created instances of + /// explorer.exe will automatically be terminated. + /// + void StartMonitoringExplorer(); + + /// + /// Stops monitoring the Windows explorer. + /// + void StopMonitoringExplorer(); + + /// + /// Terminates the Windows explorer shell, i.e. the taskbar. + /// + void CloseExplorerShell(); + } +} diff --git a/SafeExamBrowser.Contracts/Monitoring/IWindowMonitor.cs b/SafeExamBrowser.Contracts/Monitoring/IWindowMonitor.cs new file mode 100644 index 00000000..92a90720 --- /dev/null +++ b/SafeExamBrowser.Contracts/Monitoring/IWindowMonitor.cs @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2017 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +namespace SafeExamBrowser.Contracts.Monitoring +{ + public interface IWindowMonitor + { + } +} diff --git a/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj b/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj index bb6b714b..66ee8d3a 100644 --- a/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj +++ b/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj @@ -58,6 +58,10 @@ + + + + diff --git a/SafeExamBrowser.Contracts/UserInterface/ISplashScreen.cs b/SafeExamBrowser.Contracts/UserInterface/ISplashScreen.cs index 8f72e6a8..5bcad222 100644 --- a/SafeExamBrowser.Contracts/UserInterface/ISplashScreen.cs +++ b/SafeExamBrowser.Contracts/UserInterface/ISplashScreen.cs @@ -13,9 +13,14 @@ namespace SafeExamBrowser.Contracts.UserInterface public interface ISplashScreen { /// - /// Closes the splash screen. + /// Closes the splash screen on its own thread. /// - void Close(); + void InvokeClose(); + + /// + /// Shows the splash screen on its own thread. + /// + void InvokeShow(); /// /// Set the maximum of the splash screen's progress bar. @@ -23,9 +28,14 @@ namespace SafeExamBrowser.Contracts.UserInterface void SetMaxProgress(int max); /// - /// Shows the splash screen to the user. + /// Starts an animation indicating the user that something is going on. /// - void Show(); + void StartBusyIndication(); + + /// + /// Stops the busy animation, if it was running. + /// + void StopBusyIndication(); /// /// Updates the progress bar of the splash screen according to the specified amount. diff --git a/SafeExamBrowser.Contracts/UserInterface/ITaskbar.cs b/SafeExamBrowser.Contracts/UserInterface/ITaskbar.cs index d9463764..973fb184 100644 --- a/SafeExamBrowser.Contracts/UserInterface/ITaskbar.cs +++ b/SafeExamBrowser.Contracts/UserInterface/ITaskbar.cs @@ -30,10 +30,5 @@ namespace SafeExamBrowser.Contracts.UserInterface /// Sets the size of the taskbar. /// void SetSize(int width, int height); - - /// - /// Displays the taskbar on the screen. - /// - void Show(); } } diff --git a/SafeExamBrowser.Contracts/UserInterface/IUiElementFactory.cs b/SafeExamBrowser.Contracts/UserInterface/IUiElementFactory.cs index a0b4177c..55fe0277 100644 --- a/SafeExamBrowser.Contracts/UserInterface/IUiElementFactory.cs +++ b/SafeExamBrowser.Contracts/UserInterface/IUiElementFactory.cs @@ -7,6 +7,7 @@ */ using SafeExamBrowser.Contracts.Configuration; +using SafeExamBrowser.Contracts.I18n; namespace SafeExamBrowser.Contracts.UserInterface { @@ -17,6 +18,11 @@ namespace SafeExamBrowser.Contracts.UserInterface /// ITaskbarButton CreateApplicationButton(IApplicationInfo info); + /// + /// Creates a new splash screen which runs on its own thread. + /// + ISplashScreen CreateSplashScreen(ISettings settings, IText text); + /// /// Creates a taskbar notification, initialized with the given notification information. /// diff --git a/SafeExamBrowser.Core/Behaviour/ShutdownController.cs b/SafeExamBrowser.Core/Behaviour/ShutdownController.cs index b31f6f6b..fa2c3934 100644 --- a/SafeExamBrowser.Core/Behaviour/ShutdownController.cs +++ b/SafeExamBrowser.Core/Behaviour/ShutdownController.cs @@ -7,9 +7,14 @@ */ using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; using SafeExamBrowser.Contracts.Behaviour; +using SafeExamBrowser.Contracts.Configuration; using SafeExamBrowser.Contracts.I18n; using SafeExamBrowser.Contracts.Logging; +using SafeExamBrowser.Contracts.Monitoring; using SafeExamBrowser.Contracts.UserInterface; namespace SafeExamBrowser.Core.Behaviour @@ -18,23 +23,52 @@ namespace SafeExamBrowser.Core.Behaviour { private ILogger logger; private IMessageBox messageBox; + private IProcessMonitor processMonitor; + private ISettings settings; + private ISplashScreen splashScreen; private IText text; + private IUiElementFactory uiFactory; - public ShutdownController(ILogger logger, IMessageBox messageBox, IText text) + private IEnumerable ShutdownOperations + { + get + { + yield return StopProcessMonitoring; + yield return RestoreWorkArea; + yield return FinalizeApplicationLog; + } + } + + public ShutdownController( + ILogger logger, + IMessageBox messageBox, + IProcessMonitor processMonitor, + ISettings settings, + IText text, + IUiElementFactory uiFactory) { this.logger = logger; this.messageBox = messageBox; + this.processMonitor = processMonitor; + this.settings = settings; this.text = text; + this.uiFactory = uiFactory; } public void FinalizeApplication() { try { - // TODO: - // - Gather TODOs! + InitializeSplashScreen(); - logger.Log($"{Environment.NewLine}# Application terminated at {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}"); + foreach (var operation in ShutdownOperations) + { + operation(); + splashScreen.UpdateProgress(); + + // TODO: Remove! + Thread.Sleep(250); + } } catch (Exception e) { @@ -42,5 +76,41 @@ namespace SafeExamBrowser.Core.Behaviour messageBox.Show(text.Get(Key.MessageBox_ShutdownError), text.Get(Key.MessageBox_ShutdownErrorTitle), icon: MessageBoxIcon.Error); } } + + private void InitializeSplashScreen() + { + splashScreen = uiFactory.CreateSplashScreen(settings, text); + splashScreen.SetMaxProgress(ShutdownOperations.Count()); + splashScreen.UpdateText(Key.SplashScreen_ShutdownProcedure); + splashScreen.InvokeShow(); + } + + private void StopProcessMonitoring() + { + logger.Info("Stopping process monitoring."); + splashScreen.UpdateText(Key.SplashScreen_StopProcessMonitoring); + + // TODO + + processMonitor.StopMonitoringExplorer(); + } + + private void RestoreWorkArea() + { + logger.Info("Restoring work area."); + splashScreen.UpdateText(Key.SplashScreen_RestoreWorkArea); + + // TODO + + splashScreen.UpdateText(Key.SplashScreen_WaitExplorerStartup); + splashScreen.StartBusyIndication(); + processMonitor.StartExplorerShell(); + splashScreen.StopBusyIndication(); + } + + private void FinalizeApplicationLog() + { + logger.Log($"{Environment.NewLine}# Application terminated at {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}"); + } } } diff --git a/SafeExamBrowser.Core/Behaviour/StartupController.cs b/SafeExamBrowser.Core/Behaviour/StartupController.cs index 3c7b3535..5d698945 100644 --- a/SafeExamBrowser.Core/Behaviour/StartupController.cs +++ b/SafeExamBrowser.Core/Behaviour/StartupController.cs @@ -14,6 +14,7 @@ using SafeExamBrowser.Contracts.Behaviour; using SafeExamBrowser.Contracts.Configuration; using SafeExamBrowser.Contracts.I18n; using SafeExamBrowser.Contracts.Logging; +using SafeExamBrowser.Contracts.Monitoring; using SafeExamBrowser.Contracts.UserInterface; namespace SafeExamBrowser.Core.Behaviour @@ -25,6 +26,7 @@ namespace SafeExamBrowser.Core.Behaviour private ILogger logger; private IMessageBox messageBox; private INotificationInfo aboutInfo; + private IProcessMonitor processMonitor; private ISettings settings; private ISplashScreen splashScreen; private ITaskbar taskbar; @@ -53,8 +55,8 @@ namespace SafeExamBrowser.Core.Behaviour ILogger logger, IMessageBox messageBox, INotificationInfo aboutInfo, + IProcessMonitor processMonitor, ISettings settings, - ISplashScreen splashScreen, ITaskbar taskbar, IText text, IUiElementFactory uiFactory) @@ -64,8 +66,8 @@ namespace SafeExamBrowser.Core.Behaviour this.logger = logger; this.messageBox = messageBox; this.aboutInfo = aboutInfo; + this.processMonitor = processMonitor; this.settings = settings; - this.splashScreen = splashScreen; this.taskbar = taskbar; this.text = text; this.uiFactory = uiFactory; @@ -112,62 +114,63 @@ namespace SafeExamBrowser.Core.Behaviour private void InitializeSplashScreen() { + splashScreen = uiFactory.CreateSplashScreen(settings, text); splashScreen.SetMaxProgress(StartupOperations.Count()); splashScreen.UpdateText(Key.SplashScreen_StartupProcedure); + splashScreen.InvokeShow(); } private void HandleCommandLineArguments() { - logger.Info("Parsing command line arguments."); - // TODO } private void DetectOperatingSystem() { - logger.Info("Detecting operating system."); - // TODO } private void EstablishWcfServiceConnection() { - logger.Info("Establishing connection to WCF service."); - // TODO } private void DeactivateWindowsFeatures() { - logger.Info("Deactivating Windows Update."); - - // TODO - - logger.Info("Disabling lock screen options."); - // TODO } private void InitializeProcessMonitoring() { logger.Info("Initializing process monitoring."); + splashScreen.UpdateText(Key.SplashScreen_InitializeProcessMonitoring); // TODO + + processMonitor.StartMonitoringExplorer(); } private void InitializeWorkArea() { logger.Info("Initializing work area."); + splashScreen.UpdateText(Key.SplashScreen_InitializeWorkArea); // TODO - // - Killing explorer.exe // - Minimizing all open windows // - Emptying clipboard + + splashScreen.UpdateText(Key.SplashScreen_WaitExplorerTermination); + splashScreen.StartBusyIndication(); + processMonitor.CloseExplorerShell(); + splashScreen.StopBusyIndication(); } private void InitializeTaskbar() { logger.Info("Initializing taskbar."); + splashScreen.UpdateText(Key.SplashScreen_InitializeTaskbar); + + // TODO var aboutNotification = uiFactory.CreateNotification(aboutInfo); @@ -179,6 +182,8 @@ namespace SafeExamBrowser.Core.Behaviour logger.Info("Initializing browser."); splashScreen.UpdateText(Key.SplashScreen_InitializeBrowser); + // TODO + var browserButton = uiFactory.CreateApplicationButton(browserInfo); browserController.RegisterApplicationButton(browserButton); @@ -188,6 +193,7 @@ namespace SafeExamBrowser.Core.Behaviour private void FinishInitialization() { logger.Info("Application successfully initialized!"); + splashScreen.InvokeClose(); } } } diff --git a/SafeExamBrowser.Core/I18n/Text.xml b/SafeExamBrowser.Core/I18n/Text.xml index acc6147b..6629783b 100644 --- a/SafeExamBrowser.Core/I18n/Text.xml +++ b/SafeExamBrowser.Core/I18n/Text.xml @@ -5,7 +5,15 @@ An unexpected error occurred during the startup procedure! Please consult the application log for more information... Startup Error About Safe Exam Browser - Initializing browser. - Initiating startup procedure. + Initializing browser + Initializing process monitoring + Initializing taskbar + Initializing work area + Restoring work area + Initiating shutdown procedure + Initiating startup procedure + Stopping process monitoring + Waiting for Windows explorer to start up + Waiting for Windows explorer to shut down Version \ No newline at end of file diff --git a/SafeExamBrowser.Monitoring/Processes/ProcessMonitor.cs b/SafeExamBrowser.Monitoring/Processes/ProcessMonitor.cs new file mode 100644 index 00000000..2f50e75c --- /dev/null +++ b/SafeExamBrowser.Monitoring/Processes/ProcessMonitor.cs @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2017 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using SafeExamBrowser.Contracts.Logging; +using SafeExamBrowser.Contracts.Monitoring; + +namespace SafeExamBrowser.Monitoring.Processes +{ + public class ProcessMonitor : IProcessMonitor + { + private ILogger logger; + + public ProcessMonitor(ILogger logger) + { + this.logger = logger; + } + + public void StartExplorerShell() + { + var process = new Process(); + var explorerPath = Path.Combine(Environment.GetEnvironmentVariable("WINDIR"), "explorer.exe"); + + Log("Restarting explorer shell..."); + + process.StartInfo.CreateNoWindow = true; + process.StartInfo.FileName = explorerPath; + process.Start(); + + while (User32.GetShellWindowHandle() == IntPtr.Zero) + { + Thread.Sleep(20); + } + + process.Refresh(); + Log($"Explorer shell successfully started with PID = {process.Id}."); + process.Close(); + } + + public void StartMonitoringExplorer() + { + // TODO + } + + public void StopMonitoringExplorer() + { + // TODO + } + + public void CloseExplorerShell() + { + var processId = User32.GetShellProcessId(); + var explorerProcesses = Process.GetProcessesByName("explorer"); + var shellProcess = explorerProcesses.FirstOrDefault(p => p.Id == processId); + + if (shellProcess != null) + { + Log($"Found explorer shell processes with PID = {processId}. Sending close message..."); + User32.PostCloseMessageToShell(); + + while (!shellProcess.HasExited) + { + shellProcess.Refresh(); + Thread.Sleep(20); + } + + Log($"Successfully terminated explorer shell process with PID = {processId}."); + } + else + { + Log("The explorer shell seems to already be terminated. Skipping this step..."); + } + } + + private void Log(string message) + { + logger.Info($"[{nameof(ProcessMonitor)}] {message}"); + } + } +} diff --git a/SafeExamBrowser.Monitoring/Properties/AssemblyInfo.cs b/SafeExamBrowser.Monitoring/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..c51c021f --- /dev/null +++ b/SafeExamBrowser.Monitoring/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("SafeExamBrowser.Monitoring")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("SafeExamBrowser.Monitoring")] +[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("ef563531-4eb5-44b9-a5ec-d6d6f204469b")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/SafeExamBrowser.Monitoring/SafeExamBrowser.Monitoring.csproj b/SafeExamBrowser.Monitoring/SafeExamBrowser.Monitoring.csproj new file mode 100644 index 00000000..d710e122 --- /dev/null +++ b/SafeExamBrowser.Monitoring/SafeExamBrowser.Monitoring.csproj @@ -0,0 +1,59 @@ + + + + + Debug + AnyCPU + {EF563531-4EB5-44B9-A5EC-D6D6F204469B} + Library + Properties + SafeExamBrowser.Monitoring + SafeExamBrowser.Monitoring + v4.5.2 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + {47da5933-bef8-4729-94e6-abde2db12262} + SafeExamBrowser.Contracts + + + + + + + + + \ No newline at end of file diff --git a/SafeExamBrowser.Monitoring/User32.cs b/SafeExamBrowser.Monitoring/User32.cs new file mode 100644 index 00000000..eca68cd4 --- /dev/null +++ b/SafeExamBrowser.Monitoring/User32.cs @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2017 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; + +namespace SafeExamBrowser.Monitoring +{ + /// + /// Provides access to the native Windows API exposed by user32.dll. + /// + internal static class User32 + { + internal static IntPtr GetShellWindowHandle() + { + return FindWindow("Shell_TrayWnd", null); + } + + internal static uint GetShellProcessId() + { + var handle = FindWindow("Shell_TrayWnd", null); + var threadId = GetWindowThreadProcessId(handle, out uint processId); + + return processId; + } + + /// + /// The close message 0x5B4 posted to the shell is undocumented and not officially supported: + /// https://stackoverflow.com/questions/5689904/gracefully-exit-explorer-programmatically/5705965#5705965 + /// + internal static void PostCloseMessageToShell() + { + var taskbarHandle = FindWindow("Shell_TrayWnd", null); + var success = PostMessage(taskbarHandle, 0x5B4, IntPtr.Zero, IntPtr.Zero); + + if (!success) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + } + + [DllImport("user32.dll", SetLastError = true)] + private static extern IntPtr FindWindow(string lpClassName, string lpWindowName); + + [DllImport("user32.dll", SetLastError = true)] + private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); + + [return: MarshalAs(UnmanagedType.Bool)] + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern bool PostMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); + } +} diff --git a/SafeExamBrowser.UserInterface/Controls/ApplicationButton.xaml.cs b/SafeExamBrowser.UserInterface/Controls/ApplicationButton.xaml.cs index f3dba797..3a1db4b2 100644 --- a/SafeExamBrowser.UserInterface/Controls/ApplicationButton.xaml.cs +++ b/SafeExamBrowser.UserInterface/Controls/ApplicationButton.xaml.cs @@ -58,7 +58,9 @@ namespace SafeExamBrowser.UserInterface.Controls Button.ToolTip = info.Tooltip; Button.Content = IconResourceLoader.Load(info.IconResource); + Button.MouseEnter += (o, args) => InstancePopup.IsOpen = instances.Count > 1; Button.MouseLeave += (o, args) => InstancePopup.IsOpen &= InstancePopup.IsMouseOver || ActiveBar.IsMouseOver; + ActiveBar.MouseLeave += (o, args) => InstancePopup.IsOpen &= InstancePopup.IsMouseOver || Button.IsMouseOver; InstancePopup.MouseLeave += (o, args) => InstancePopup.IsOpen = false; InstancePopup.Opened += (o, args) => ActiveBar.Width = Double.NaN; InstancePopup.Closed += (o, args) => ActiveBar.Width = 40; diff --git a/SafeExamBrowser.UserInterface/Controls/QuitButton.xaml.cs b/SafeExamBrowser.UserInterface/Controls/QuitButton.xaml.cs index 71315caa..77a3e3d2 100644 --- a/SafeExamBrowser.UserInterface/Controls/QuitButton.xaml.cs +++ b/SafeExamBrowser.UserInterface/Controls/QuitButton.xaml.cs @@ -20,7 +20,7 @@ namespace SafeExamBrowser.UserInterface.Controls private void Button_Click(object sender, RoutedEventArgs e) { - Application.Current.Shutdown(); + Application.Current.MainWindow.Close(); } } } diff --git a/SafeExamBrowser.UserInterface/SplashScreen.xaml.cs b/SafeExamBrowser.UserInterface/SplashScreen.xaml.cs index 0fc74c58..e6c03749 100644 --- a/SafeExamBrowser.UserInterface/SplashScreen.xaml.cs +++ b/SafeExamBrowser.UserInterface/SplashScreen.xaml.cs @@ -30,11 +30,31 @@ namespace SafeExamBrowser.UserInterface InitializeSplashScreen(); } + public void InvokeClose() + { + Dispatcher.Invoke(Close); + } + + public void InvokeShow() + { + Dispatcher.Invoke(Show); + } + public void SetMaxProgress(int max) { model.MaxProgress = max; } + public void StartBusyIndication() + { + model.StartBusyIndication(); + } + + public void StopBusyIndication() + { + model.StopBusyIndication(); + } + public void UpdateProgress(int amount = 1) { model.CurrentProgress += amount; diff --git a/SafeExamBrowser.UserInterface/UiElementFactory.cs b/SafeExamBrowser.UserInterface/UiElementFactory.cs index a0f00f69..9956843c 100644 --- a/SafeExamBrowser.UserInterface/UiElementFactory.cs +++ b/SafeExamBrowser.UserInterface/UiElementFactory.cs @@ -6,7 +6,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +using System.Threading; using SafeExamBrowser.Contracts.Configuration; +using SafeExamBrowser.Contracts.I18n; using SafeExamBrowser.Contracts.UserInterface; using SafeExamBrowser.UserInterface.Controls; @@ -19,6 +21,31 @@ namespace SafeExamBrowser.UserInterface return new ApplicationButton(info); } + public ISplashScreen CreateSplashScreen(ISettings settings, IText text) + { + SplashScreen splashScreen = null; + var splashReadyEvent = new AutoResetEvent(false); + var splashScreenThread = new Thread(() => + { + splashScreen = new SplashScreen(settings, text); + splashScreen.Closed += (o, args) => splashScreen.Dispatcher.InvokeShutdown(); + splashScreen.Show(); + + splashReadyEvent.Set(); + + System.Windows.Threading.Dispatcher.Run(); + }); + + splashScreenThread.SetApartmentState(ApartmentState.STA); + splashScreenThread.Name = "Splash Screen Thread"; + splashScreenThread.IsBackground = true; + splashScreenThread.Start(); + + splashReadyEvent.WaitOne(); + + return splashScreen; + } + public ITaskbarNotification CreateNotification(INotificationInfo info) { return new NotificationIcon(info); diff --git a/SafeExamBrowser.UserInterface/ViewModels/SplashScreenViewModel.cs b/SafeExamBrowser.UserInterface/ViewModels/SplashScreenViewModel.cs index e3d858c2..bcd474ad 100644 --- a/SafeExamBrowser.UserInterface/ViewModels/SplashScreenViewModel.cs +++ b/SafeExamBrowser.UserInterface/ViewModels/SplashScreenViewModel.cs @@ -7,6 +7,7 @@ */ using System.ComponentModel; +using System.Timers; namespace SafeExamBrowser.UserInterface.ViewModels { @@ -15,6 +16,7 @@ namespace SafeExamBrowser.UserInterface.ViewModels private int currentProgress; private int maxProgress; private string status; + private Timer busyTimer; public event PropertyChangedEventHandler PropertyChanged; @@ -56,5 +58,41 @@ namespace SafeExamBrowser.UserInterface.ViewModels PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Status))); } } + + public void StartBusyIndication() + { + StopBusyIndication(); + + busyTimer = new Timer + { + AutoReset = true, + Interval = 750 + }; + + busyTimer.Elapsed += BusyTimer_Elapsed; + busyTimer.Start(); + } + + public void StopBusyIndication() + { + busyTimer?.Stop(); + busyTimer?.Close(); + } + + private void BusyTimer_Elapsed(object sender, ElapsedEventArgs e) + { + var next = Status ?? string.Empty; + + if (next.EndsWith("...")) + { + next = Status.Substring(0, Status.Length - 3); + } + else + { + next += "."; + } + + Status = next; + } } } diff --git a/SafeExamBrowser.sln b/SafeExamBrowser.sln index d36f315a..70768e7c 100644 --- a/SafeExamBrowser.sln +++ b/SafeExamBrowser.sln @@ -21,6 +21,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SafeExamBrowser.Contracts", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SafeExamBrowser.Browser", "SafeExamBrowser.Browser\SafeExamBrowser.Browser.csproj", "{04E653F1-98E6-4E34-9DD7-7F2BC1A8B767}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SafeExamBrowser.Monitoring", "SafeExamBrowser.Monitoring\SafeExamBrowser.Monitoring.csproj", "{EF563531-4EB5-44B9-A5EC-D6D6F204469B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -51,6 +53,10 @@ Global {04E653F1-98E6-4E34-9DD7-7F2BC1A8B767}.Debug|Any CPU.Build.0 = Debug|Any CPU {04E653F1-98E6-4E34-9DD7-7F2BC1A8B767}.Release|Any CPU.ActiveCfg = Release|Any CPU {04E653F1-98E6-4E34-9DD7-7F2BC1A8B767}.Release|Any CPU.Build.0 = Release|Any CPU + {EF563531-4EB5-44B9-A5EC-D6D6F204469B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EF563531-4EB5-44B9-A5EC-D6D6F204469B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EF563531-4EB5-44B9-A5EC-D6D6F204469B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EF563531-4EB5-44B9-A5EC-D6D6F204469B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/SafeExamBrowser/App.cs b/SafeExamBrowser/App.cs index 99fd1492..7cc469c5 100644 --- a/SafeExamBrowser/App.cs +++ b/SafeExamBrowser/App.cs @@ -26,7 +26,11 @@ namespace SafeExamBrowser } catch (Exception e) { - MessageBox.Show(e.Message + "\n\n" + e.StackTrace, "Fatal Error"); + MessageBox.Show(e.Message + "\n\n" + e.StackTrace, "Fatal Error", MessageBoxButton.OK, MessageBoxImage.Error); + } + finally + { + mutex.Close(); } } @@ -38,7 +42,7 @@ namespace SafeExamBrowser } else { - MessageBox.Show("You can only run one instance of SEB at a time.", "Startup Not Allowed"); + MessageBox.Show("You can only run one instance of SEB at a time.", "Startup Not Allowed", MessageBoxButton.OK, MessageBoxImage.Information); } } @@ -51,43 +55,6 @@ namespace SafeExamBrowser { base.OnStartup(e); - ShowSplashScreen(); - InitializeApplication(); - } - - protected override void OnExit(ExitEventArgs e) - { - instances.ShutdownController.FinalizeApplication(); - - base.OnExit(e); - } - - private void ShowSplashScreen() - { - instances.BuildModulesRequiredBySplashScreen(); - - var splashReadyEvent = new AutoResetEvent(false); - var splashScreenThread = new Thread(() => - { - instances.SplashScreen = new UserInterface.SplashScreen(instances.Settings, instances.Text); - instances.SplashScreen.Closed += (o, args) => instances.SplashScreen.Dispatcher.InvokeShutdown(); - instances.SplashScreen.Show(); - - splashReadyEvent.Set(); - - System.Windows.Threading.Dispatcher.Run(); - }); - - splashScreenThread.SetApartmentState(ApartmentState.STA); - splashScreenThread.Name = "Splash Screen Thread"; - splashScreenThread.IsBackground = true; - splashScreenThread.Start(); - - splashReadyEvent.WaitOne(); - } - - private void InitializeApplication() - { instances.BuildObjectGraph(); var success = instances.StartupController.TryInitializeApplication(); @@ -95,14 +62,19 @@ namespace SafeExamBrowser if (success) { MainWindow = instances.Taskbar; + MainWindow.Closing += (o, args) => ShutdownApplication(); MainWindow.Show(); } else { Shutdown(); } + } - instances.SplashScreen?.Dispatcher.InvokeAsync(instances.SplashScreen.Close); + private void ShutdownApplication() + { + MainWindow.Hide(); + instances.ShutdownController.FinalizeApplication(); } } } diff --git a/SafeExamBrowser/CompositionRoot.cs b/SafeExamBrowser/CompositionRoot.cs index ce6f5c55..7b7af9fe 100644 --- a/SafeExamBrowser/CompositionRoot.cs +++ b/SafeExamBrowser/CompositionRoot.cs @@ -11,11 +11,13 @@ using SafeExamBrowser.Contracts.Behaviour; using SafeExamBrowser.Contracts.Configuration; using SafeExamBrowser.Contracts.I18n; using SafeExamBrowser.Contracts.Logging; +using SafeExamBrowser.Contracts.Monitoring; using SafeExamBrowser.Contracts.UserInterface; using SafeExamBrowser.Core.Behaviour; using SafeExamBrowser.Core.Configuration; using SafeExamBrowser.Core.I18n; using SafeExamBrowser.Core.Logging; +using SafeExamBrowser.Monitoring.Processes; using SafeExamBrowser.UserInterface; namespace SafeExamBrowser @@ -24,23 +26,18 @@ namespace SafeExamBrowser { private IApplicationController browserController; private IApplicationInfo browserInfo; + private ILogger logger; private IMessageBox messageBox; private INotificationInfo aboutInfo; - private ILogger logger; + private IProcessMonitor processMonitor; + private ISettings settings; + private IText text; private IUiElementFactory uiFactory; + private ITextResource textResource; - public ISettings Settings { get; private set; } public IShutdownController ShutdownController { get; private set; } public IStartupController StartupController { get; private set; } - public SplashScreen SplashScreen { get; set; } public Taskbar Taskbar { get; private set; } - public IText Text { get; private set; } - - public void BuildModulesRequiredBySplashScreen() - { - Settings = new Settings(); - Text = new Text(new XmlTextResource()); - } public void BuildObjectGraph() { @@ -48,14 +45,18 @@ namespace SafeExamBrowser browserInfo = new BrowserApplicationInfo(); logger = new Logger(); messageBox = new WpfMessageBox(); + settings = new Settings(); Taskbar = new Taskbar(); + textResource = new XmlTextResource(); uiFactory = new UiElementFactory(); - logger.Subscribe(new LogFileWriter(Settings)); + logger.Subscribe(new LogFileWriter(settings)); - aboutInfo = new AboutNotificationInfo(Text); - ShutdownController = new ShutdownController(logger, messageBox, Text); - StartupController = new StartupController(browserController, browserInfo, logger, messageBox, aboutInfo, Settings, SplashScreen, Taskbar, Text, uiFactory); + text = new Text(textResource); + aboutInfo = new AboutNotificationInfo(text); + processMonitor = new ProcessMonitor(logger); + ShutdownController = new ShutdownController(logger, messageBox, processMonitor, settings, text, uiFactory); + StartupController = new StartupController(browserController, browserInfo, logger, messageBox, aboutInfo, processMonitor, settings, Taskbar, text, uiFactory); } } } diff --git a/SafeExamBrowser/SafeExamBrowser.csproj b/SafeExamBrowser/SafeExamBrowser.csproj index af8765a8..044aa236 100644 --- a/SafeExamBrowser/SafeExamBrowser.csproj +++ b/SafeExamBrowser/SafeExamBrowser.csproj @@ -111,6 +111,10 @@ {3d6fdbb6-a4af-4626-bb2b-bf329d44f9cc} SafeExamBrowser.Core + + {ef563531-4eb5-44b9-a5ec-d6d6f204469b} + SafeExamBrowser.Monitoring + {e1be031a-4354-41e7-83e8-843ded4489ff} SafeExamBrowser.UserInterface