diff --git a/SafeExamBrowser.Client.UnitTests/Operations/WindowMonitorOperationTests.cs b/SafeExamBrowser.Client.UnitTests/Operations/WindowMonitorOperationTests.cs index 3a9aff8e..9b9c59b7 100644 --- a/SafeExamBrowser.Client.UnitTests/Operations/WindowMonitorOperationTests.cs +++ b/SafeExamBrowser.Client.UnitTests/Operations/WindowMonitorOperationTests.cs @@ -9,6 +9,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using SafeExamBrowser.Client.Operations; +using SafeExamBrowser.Contracts.Configuration.Settings; using SafeExamBrowser.Contracts.Logging; using SafeExamBrowser.Contracts.Monitoring; @@ -20,21 +21,58 @@ namespace SafeExamBrowser.Client.UnitTests.Operations private Mock loggerMock; private Mock windowMonitorMock; - private WindowMonitorOperation sut; - [TestInitialize] public void Initialize() { loggerMock = new Mock(); windowMonitorMock = new Mock(); - - sut = new WindowMonitorOperation(loggerMock.Object, windowMonitorMock.Object); } [TestMethod] - public void MustPerformCorrectly() + public void MustPerformCorrectlyForCreateNewDesktop() { var order = 0; + var hideAll = 0; + var startMonitoring = 0; + var sut = new WindowMonitorOperation(KioskMode.CreateNewDesktop, loggerMock.Object, windowMonitorMock.Object); + + windowMonitorMock.Setup(w => w.HideAllWindows()).Callback(() => hideAll = ++order); + windowMonitorMock.Setup(w => w.StartMonitoringWindows()).Callback(() => startMonitoring = ++order); + + sut.Perform(); + + windowMonitorMock.Verify(w => w.HideAllWindows(), Times.Never); + windowMonitorMock.Verify(w => w.StartMonitoringWindows(), Times.Once); + + Assert.AreEqual(0, hideAll); + Assert.AreEqual(1, startMonitoring); + } + + [TestMethod] + public void MustRevertCorrectlyForCreateNewDesktop() + { + var order = 0; + var stop = 0; + var restore = 0; + var sut = new WindowMonitorOperation(KioskMode.CreateNewDesktop, loggerMock.Object, windowMonitorMock.Object); + + windowMonitorMock.Setup(w => w.StopMonitoringWindows()).Callback(() => stop = ++order); + windowMonitorMock.Setup(w => w.RestoreHiddenWindows()).Callback(() => restore = ++order); + + sut.Revert(); + + windowMonitorMock.Verify(w => w.StopMonitoringWindows(), Times.Once); + windowMonitorMock.Verify(w => w.RestoreHiddenWindows(), Times.Never); + + Assert.AreEqual(0, restore); + Assert.AreEqual(1, stop); + } + + [TestMethod] + public void MustPerformCorrectlyForDisableExplorerShell() + { + var order = 0; + var sut = new WindowMonitorOperation(KioskMode.DisableExplorerShell, loggerMock.Object, windowMonitorMock.Object); windowMonitorMock.Setup(w => w.HideAllWindows()).Callback(() => Assert.AreEqual(++order, 1)); windowMonitorMock.Setup(w => w.StartMonitoringWindows()).Callback(() => Assert.AreEqual(++order, 2)); @@ -46,9 +84,10 @@ namespace SafeExamBrowser.Client.UnitTests.Operations } [TestMethod] - public void MustRevertCorrectly() + public void MustRevertCorrectlyForDisableExplorerShell() { var order = 0; + var sut = new WindowMonitorOperation(KioskMode.DisableExplorerShell, loggerMock.Object, windowMonitorMock.Object); windowMonitorMock.Setup(w => w.StopMonitoringWindows()).Callback(() => Assert.AreEqual(++order, 1)); windowMonitorMock.Setup(w => w.RestoreHiddenWindows()).Callback(() => Assert.AreEqual(++order, 2)); @@ -58,5 +97,13 @@ namespace SafeExamBrowser.Client.UnitTests.Operations windowMonitorMock.Verify(w => w.StopMonitoringWindows(), Times.Once); windowMonitorMock.Verify(w => w.RestoreHiddenWindows(), Times.Once); } + + [TestMethod] + public void MustDoNothingWithoutKioskMode() + { + var sut = new WindowMonitorOperation(KioskMode.None, loggerMock.Object, windowMonitorMock.Object); + + windowMonitorMock.VerifyNoOtherCalls(); + } } } diff --git a/SafeExamBrowser.Client/ClientController.cs b/SafeExamBrowser.Client/ClientController.cs index 07f47a62..a21258c2 100644 --- a/SafeExamBrowser.Client/ClientController.cs +++ b/SafeExamBrowser.Client/ClientController.cs @@ -307,17 +307,19 @@ namespace SafeExamBrowser.Client private void WindowMonitor_WindowChanged(IntPtr window) { - var allowed = processMonitor.BelongsToAllowedProcess(window); + // TODO! - if (!allowed) - { - var success = windowMonitor.Hide(window); + //var allowed = processMonitor.BelongsToAllowedProcess(window); - if (!success) - { - windowMonitor.Close(window); - } - } + //if (!allowed) + //{ + // var success = windowMonitor.Hide(window); + + // if (!success) + // { + // windowMonitor.Close(window); + // } + //} } } } diff --git a/SafeExamBrowser.Client/CompositionRoot.cs b/SafeExamBrowser.Client/CompositionRoot.cs index 0c934145..17f7d837 100644 --- a/SafeExamBrowser.Client/CompositionRoot.cs +++ b/SafeExamBrowser.Client/CompositionRoot.cs @@ -63,6 +63,7 @@ namespace SafeExamBrowser.Client private IText text; private ITextResource textResource; private IUserInterfaceFactory uiFactory; + private IWindowMonitor windowMonitor; internal IClientController ClientController { get; private set; } internal Taskbar Taskbar { get; private set; } @@ -83,10 +84,10 @@ namespace SafeExamBrowser.Client processMonitor = new ProcessMonitor(new ModuleLogger(logger, nameof(ProcessMonitor)), nativeMethods); uiFactory = new UserInterfaceFactory(text); runtimeProxy = new RuntimeProxy(runtimeHostUri, new ProxyObjectFactory(), new ModuleLogger(logger, nameof(RuntimeProxy))); + windowMonitor = new WindowMonitor(new ModuleLogger(logger, nameof(WindowMonitor)), nativeMethods); var displayMonitor = new DisplayMonitor(new ModuleLogger(logger, nameof(DisplayMonitor)), nativeMethods); var explorerShell = new ExplorerShell(new ModuleLogger(logger, nameof(ExplorerShell)), nativeMethods); - var windowMonitor = new WindowMonitor(new ModuleLogger(logger, nameof(WindowMonitor)), nativeMethods); Taskbar = new Taskbar(new ModuleLogger(logger, nameof(Taskbar))); @@ -98,8 +99,7 @@ namespace SafeExamBrowser.Client operations.Enqueue(new DelegateOperation(UpdateAppConfig)); operations.Enqueue(new LazyInitializationOperation(BuildCommunicationHostOperation)); operations.Enqueue(new LazyInitializationOperation(BuildKeyboardInterceptorOperation)); - // TODO - //operations.Enqueue(new WindowMonitorOperation(logger, windowMonitor)); + operations.Enqueue(new LazyInitializationOperation(BuildWindowMonitorOperation)); operations.Enqueue(new LazyInitializationOperation(BuildProcessMonitorOperation)); operations.Enqueue(new DisplayMonitorOperation(displayMonitor, logger, Taskbar)); operations.Enqueue(new LazyInitializationOperation(BuildTaskbarOperation)); @@ -223,6 +223,11 @@ namespace SafeExamBrowser.Client return operation; } + private IOperation BuildWindowMonitorOperation() + { + return new WindowMonitorOperation(configuration.Settings.KioskMode, logger, windowMonitor); + } + private void UpdateAppConfig() { ClientController.AppConfig = configuration.AppConfig; diff --git a/SafeExamBrowser.Client/Operations/WindowMonitorOperation.cs b/SafeExamBrowser.Client/Operations/WindowMonitorOperation.cs index 1f1b0c78..0cdcc4fa 100644 --- a/SafeExamBrowser.Client/Operations/WindowMonitorOperation.cs +++ b/SafeExamBrowser.Client/Operations/WindowMonitorOperation.cs @@ -6,6 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +using SafeExamBrowser.Contracts.Configuration.Settings; using SafeExamBrowser.Contracts.Core.OperationModel; using SafeExamBrowser.Contracts.I18n; using SafeExamBrowser.Contracts.Logging; @@ -16,13 +17,15 @@ namespace SafeExamBrowser.Client.Operations { internal class WindowMonitorOperation : IOperation { + private KioskMode kioskMode; private ILogger logger; private IWindowMonitor windowMonitor; public IProgressIndicator ProgressIndicator { private get; set; } - public WindowMonitorOperation(ILogger logger, IWindowMonitor windowMonitor) + public WindowMonitorOperation(KioskMode kioskMode, ILogger logger, IWindowMonitor windowMonitor) { + this.kioskMode = kioskMode; this.logger = logger; this.windowMonitor = windowMonitor; } @@ -32,8 +35,15 @@ namespace SafeExamBrowser.Client.Operations logger.Info("Initializing window monitoring..."); ProgressIndicator?.UpdateText(TextKey.ProgressIndicator_InitializeWindowMonitoring); - windowMonitor.HideAllWindows(); - windowMonitor.StartMonitoringWindows(); + if (kioskMode == KioskMode.DisableExplorerShell) + { + windowMonitor.HideAllWindows(); + } + + if (kioskMode != KioskMode.None) + { + windowMonitor.StartMonitoringWindows(); + } return OperationResult.Success; } @@ -48,8 +58,15 @@ namespace SafeExamBrowser.Client.Operations logger.Info("Stopping window monitoring..."); ProgressIndicator?.UpdateText(TextKey.ProgressIndicator_StopWindowMonitoring); - windowMonitor.StopMonitoringWindows(); - windowMonitor.RestoreHiddenWindows(); + if (kioskMode != KioskMode.None) + { + windowMonitor.StopMonitoringWindows(); + } + + if (kioskMode == KioskMode.DisableExplorerShell) + { + windowMonitor.RestoreHiddenWindows(); + } } } } diff --git a/SafeExamBrowser.Communication.UnitTests/Proxies/BaseProxyTests.cs b/SafeExamBrowser.Communication.UnitTests/Proxies/BaseProxyTests.cs index 5a4e31de..f1965eff 100644 --- a/SafeExamBrowser.Communication.UnitTests/Proxies/BaseProxyTests.cs +++ b/SafeExamBrowser.Communication.UnitTests/Proxies/BaseProxyTests.cs @@ -120,6 +120,24 @@ namespace SafeExamBrowser.Communication.UnitTests.Proxies Assert.IsFalse(connected); } + [TestMethod] + public void MustHandleMissingEndpointCorrectly() + { + var proxy = new Mock(); + + proxyObjectFactory.Setup(f => f.CreateObject(It.IsAny())).Throws(); + + var token = Guid.NewGuid(); + var connected = sut.Connect(token); + + logger.Verify(l => l.Warn(It.IsAny()), Times.AtLeastOnce()); + logger.Verify(l => l.Error(It.IsAny()), Times.Never()); + logger.Verify(l => l.Error(It.IsAny(), It.IsAny()), Times.Never()); + proxyObjectFactory.Verify(f => f.CreateObject(It.IsAny()), Times.Once); + + Assert.IsFalse(connected); + } + [TestMethod] public void MustFailToDisconnectIfNotConnected() { diff --git a/SafeExamBrowser.Communication/Hosts/BaseHost.cs b/SafeExamBrowser.Communication/Hosts/BaseHost.cs index b8825c33..fe4531e0 100644 --- a/SafeExamBrowser.Communication/Hosts/BaseHost.cs +++ b/SafeExamBrowser.Communication/Hosts/BaseHost.cs @@ -22,7 +22,7 @@ namespace SafeExamBrowser.Communication.Hosts [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Single)] public abstract class BaseHost : ICommunication, ICommunicationHost { - private const int TWO_SECONDS = 2000; + private const int FIVE_SECONDS = 5000; private readonly object @lock = new object(); private string address; @@ -137,11 +137,11 @@ namespace SafeExamBrowser.Communication.Hosts hostThread.IsBackground = true; hostThread.Start(); - var success = startedEvent.WaitOne(TWO_SECONDS); + var success = startedEvent.WaitOne(FIVE_SECONDS); if (!success) { - throw new CommunicationException($"Failed to start communication host for endpoint '{address}' within {TWO_SECONDS / 1000} seconds!", exception); + throw new CommunicationException($"Failed to start communication host for endpoint '{address}' within {FIVE_SECONDS / 1000} seconds!", exception); } } } @@ -202,7 +202,7 @@ namespace SafeExamBrowser.Communication.Hosts try { host?.Close(); - success = hostThread?.Join(TWO_SECONDS) == true; + success = hostThread?.Join(FIVE_SECONDS) == true; } catch (Exception e) { diff --git a/SafeExamBrowser.Communication/Proxies/BaseProxy.cs b/SafeExamBrowser.Communication/Proxies/BaseProxy.cs index d3e5cb99..a23e5b95 100644 --- a/SafeExamBrowser.Communication/Proxies/BaseProxy.cs +++ b/SafeExamBrowser.Communication/Proxies/BaseProxy.cs @@ -64,12 +64,16 @@ namespace SafeExamBrowser.Communication.Proxies return success; } + catch (EndpointNotFoundException) + { + Logger.Warn($"Endpoint '{address}' could not be found!"); + } catch (Exception e) { Logger.Error($"Failed to connect to endpoint '{address}'!", e); - - return false; } + + return false; } public virtual bool Disconnect() @@ -209,7 +213,7 @@ namespace SafeExamBrowser.Communication.Proxies private void BaseProxy_Faulted(object sender, EventArgs e) { - Logger.Error("Communication channel has faulted!"); + Logger.Warn("Communication channel has faulted!"); } private void BaseProxy_Opened(object sender, EventArgs e) diff --git a/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj b/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj index b2e26c86..d776f3c4 100644 --- a/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj +++ b/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj @@ -181,6 +181,7 @@ + diff --git a/SafeExamBrowser.Contracts/WindowsApi/Events/SystemEventCallback.cs b/SafeExamBrowser.Contracts/WindowsApi/Events/SystemEventCallback.cs new file mode 100644 index 00000000..208a3273 --- /dev/null +++ b/SafeExamBrowser.Contracts/WindowsApi/Events/SystemEventCallback.cs @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2018 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; + +namespace SafeExamBrowser.Contracts.WindowsApi.Events +{ + /// + /// Defines the callback for system event hooks. + /// + public delegate void SystemEventCallback(IntPtr window); +} diff --git a/SafeExamBrowser.Contracts/WindowsApi/INativeMethods.cs b/SafeExamBrowser.Contracts/WindowsApi/INativeMethods.cs index e1351a5a..476094b6 100644 --- a/SafeExamBrowser.Contracts/WindowsApi/INativeMethods.cs +++ b/SafeExamBrowser.Contracts/WindowsApi/INativeMethods.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using SafeExamBrowser.Contracts.Monitoring; +using SafeExamBrowser.Contracts.WindowsApi.Events; namespace SafeExamBrowser.Contracts.WindowsApi { @@ -34,12 +35,12 @@ namespace SafeExamBrowser.Contracts.WindowsApi void DeregisterMouseHook(IMouseInterceptor interceptor); /// - /// Deregisters a previously registered system event. + /// Deregisters a previously registered system event hook. /// /// /// If the event hook could not be successfully removed. /// - void DeregisterSystemEvent(IntPtr handle); + void DeregisterSystemEventHook(Guid hookId); /// /// Empties the clipboard. @@ -128,17 +129,17 @@ namespace SafeExamBrowser.Contracts.WindowsApi /// void RegisterMouseHook(IMouseInterceptor interceptor); + /// + /// Registers a system event which will invoke the specified callback when a window has received mouse capture. + /// Returns the ID of the newly registered Windows event hook. + /// + Guid RegisterSystemCaptureStartEvent(SystemEventCallback callback); + /// /// Registers a system event which will invoke the specified callback when the foreground window has changed. /// Returns a handle to the newly registered Windows event hook. /// - IntPtr RegisterSystemForegroundEvent(Action callback); - - /// - /// Registers a system event which will invoke the specified callback when a window has received mouse capture. - /// Returns a handle to the newly registered Windows event hook. - /// - IntPtr RegisterSystemCaptureStartEvent(Action callback); + Guid RegisterSystemForegroundEvent(SystemEventCallback callback); /// /// Removes the currently configured desktop wallpaper. diff --git a/SafeExamBrowser.Logging/DefaultLogFormatter.cs b/SafeExamBrowser.Logging/DefaultLogFormatter.cs index 62f156db..b9dc08db 100644 --- a/SafeExamBrowser.Logging/DefaultLogFormatter.cs +++ b/SafeExamBrowser.Logging/DefaultLogFormatter.cs @@ -35,9 +35,11 @@ namespace SafeExamBrowser.Logging { var date = message.DateTime.ToString("yyyy-MM-dd HH:mm:ss.fff"); var severity = message.Severity.ToString().ToUpper(); - var threadInfo = $"{message.ThreadInfo.Id}{(message.ThreadInfo.HasName ? ": " + message.ThreadInfo.Name : string.Empty)}"; + var threadId = message.ThreadInfo.Id < 10 ? $"0{message.ThreadInfo.Id}" : message.ThreadInfo.Id.ToString(); + var threadName = message.ThreadInfo.HasName ? ": " + message.ThreadInfo.Name : string.Empty; + var threadInfo = $"[{threadId}{threadName}]"; - return $"{date} [{threadInfo}] - {severity}: {message.Message}"; + return $"{date} {threadInfo} - {severity}: {message.Message}"; } } } diff --git a/SafeExamBrowser.Monitoring/Windows/WindowMonitor.cs b/SafeExamBrowser.Monitoring/Windows/WindowMonitor.cs index c82ce673..502709a5 100644 --- a/SafeExamBrowser.Monitoring/Windows/WindowMonitor.cs +++ b/SafeExamBrowser.Monitoring/Windows/WindowMonitor.cs @@ -17,8 +17,9 @@ namespace SafeExamBrowser.Monitoring.Windows { public class WindowMonitor : IWindowMonitor { - private IntPtr captureStartHookHandle; - private IntPtr foregroundHookHandle; + private IntPtr activeWindow; + private Guid? captureHookId; + private Guid? foregroundHookId; private ILogger logger; private IList minimizedWindows = new List(); private INativeMethods nativeMethods; @@ -92,31 +93,36 @@ namespace SafeExamBrowser.Monitoring.Windows public void StartMonitoringWindows() { - captureStartHookHandle = nativeMethods.RegisterSystemCaptureStartEvent(OnWindowChanged); - logger.Info($"Registered system capture start event with handle = {captureStartHookHandle}."); + captureHookId = nativeMethods.RegisterSystemCaptureStartEvent(OnWindowChanged); + logger.Info($"Registered system capture start event with ID = {captureHookId}."); - foregroundHookHandle = nativeMethods.RegisterSystemForegroundEvent(OnWindowChanged); - logger.Info($"Registered system foreground event with handle = {foregroundHookHandle}."); + foregroundHookId = nativeMethods.RegisterSystemForegroundEvent(OnWindowChanged); + logger.Info($"Registered system foreground event with ID = {foregroundHookId}."); } public void StopMonitoringWindows() { - if (captureStartHookHandle != IntPtr.Zero) + if (captureHookId.HasValue) { - nativeMethods.DeregisterSystemEvent(captureStartHookHandle); - logger.Info($"Unregistered system capture start event with handle = {captureStartHookHandle}."); + nativeMethods.DeregisterSystemEventHook(captureHookId.Value); + logger.Info($"Unregistered system capture start event with ID = {captureHookId}."); } - if (foregroundHookHandle != IntPtr.Zero) + if (foregroundHookId.HasValue) { - nativeMethods.DeregisterSystemEvent(foregroundHookHandle); - logger.Info($"Unregistered system foreground event with handle = {foregroundHookHandle}."); + nativeMethods.DeregisterSystemEventHook(foregroundHookId.Value); + logger.Info($"Unregistered system foreground event with ID = {foregroundHookId}."); } } private void OnWindowChanged(IntPtr window) { - WindowChanged?.Invoke(window); + if (activeWindow != window) + { + logger.Debug($"Window has changed from {activeWindow} to {window}."); + activeWindow = window; + WindowChanged?.Invoke(window); + } } } } diff --git a/SafeExamBrowser.Runtime/CompositionRoot.cs b/SafeExamBrowser.Runtime/CompositionRoot.cs index 04b05fcf..bd4aa9da 100644 --- a/SafeExamBrowser.Runtime/CompositionRoot.cs +++ b/SafeExamBrowser.Runtime/CompositionRoot.cs @@ -41,7 +41,7 @@ namespace SafeExamBrowser.Runtime internal void BuildObjectGraph(Action shutdown) { - const int STARTUP_TIMEOUT_MS = 30000; + const int STARTUP_TIMEOUT_MS = 15000; var args = Environment.GetCommandLineArgs(); var configuration = BuildConfigurationRepository(); diff --git a/SafeExamBrowser.Runtime/RuntimeController.cs b/SafeExamBrowser.Runtime/RuntimeController.cs index 3ec11cad..6a63cdfb 100644 --- a/SafeExamBrowser.Runtime/RuntimeController.cs +++ b/SafeExamBrowser.Runtime/RuntimeController.cs @@ -139,7 +139,7 @@ namespace SafeExamBrowser.Runtime runtimeWindow.Show(); runtimeWindow.BringToForeground(); runtimeWindow.ShowProgressBar(); - logger.Info("Initiating session procedure..."); + logger.Info("### --- Session Start Procedure --- ###"); if (sessionRunning) { @@ -152,7 +152,7 @@ namespace SafeExamBrowser.Runtime { RegisterSessionEvents(); - logger.Info("Session is running."); + logger.Info("### --- Session Running --- ###"); runtimeWindow.HideProgressBar(); runtimeWindow.UpdateText(TextKey.RuntimeWindow_ApplicationRunning); runtimeWindow.TopMost = configuration.CurrentSettings.KioskMode != KioskMode.None; @@ -166,7 +166,7 @@ namespace SafeExamBrowser.Runtime } else { - logger.Info($"Session procedure {(result == OperationResult.Aborted ? "was aborted." : "has failed!")}"); + logger.Info($"### --- Session Start {(result == OperationResult.Aborted ? "Aborted" : "Failed")} --- ###"); if (result == OperationResult.Failed) { @@ -187,7 +187,7 @@ namespace SafeExamBrowser.Runtime runtimeWindow.Show(); runtimeWindow.BringToForeground(); runtimeWindow.ShowProgressBar(); - logger.Info("Reverting session operations..."); + logger.Info("### --- Session Stop Procedure --- ###"); DeregisterSessionEvents(); @@ -195,12 +195,12 @@ namespace SafeExamBrowser.Runtime if (success) { - logger.Info("Session is terminated."); + logger.Info("### --- Session Terminated --- ###"); sessionRunning = false; } else { - logger.Info("Session reversion was erroneous!"); + logger.Info("### --- Session Stop Failed --- ###"); messageBox.Show(TextKey.MessageBox_SessionStopError, TextKey.MessageBox_SessionStopErrorTitle, icon: MessageBoxIcon.Error); } } diff --git a/SafeExamBrowser.UserInterface.Classic/ViewModels/LogViewModel.cs b/SafeExamBrowser.UserInterface.Classic/ViewModels/LogViewModel.cs index 1fe94eec..4b8cee73 100644 --- a/SafeExamBrowser.UserInterface.Classic/ViewModels/LogViewModel.cs +++ b/SafeExamBrowser.UserInterface.Classic/ViewModels/LogViewModel.cs @@ -70,9 +70,11 @@ namespace SafeExamBrowser.UserInterface.Classic.ViewModels { var date = message.DateTime.ToString("yyyy-MM-dd HH:mm:ss.fff"); var severity = message.Severity.ToString().ToUpper(); - var threadInfo = $"{message.ThreadInfo.Id}{(message.ThreadInfo.HasName ? ": " + message.ThreadInfo.Name : string.Empty)}"; + var threadId = message.ThreadInfo.Id < 10 ? $"0{message.ThreadInfo.Id}" : message.ThreadInfo.Id.ToString(); + var threadName = message.ThreadInfo.HasName ? ": " + message.ThreadInfo.Name : string.Empty; + var threadInfo = $"[{threadId}{threadName}]"; - var infoRun = new Run($"{date} [{threadInfo}] - ") { Foreground = Brushes.Gray }; + var infoRun = new Run($"{date} {threadInfo} - ") { Foreground = Brushes.Gray }; var messageRun = new Run($"{severity}: {message.Message}{Environment.NewLine}") { Foreground = GetBrushFor(message.Severity) }; textBlock.Inlines.Add(infoRun); diff --git a/SafeExamBrowser.WindowsApi/Monitoring/KeyboardHook.cs b/SafeExamBrowser.WindowsApi/Monitoring/KeyboardHook.cs index 36c3a386..259429c3 100644 --- a/SafeExamBrowser.WindowsApi/Monitoring/KeyboardHook.cs +++ b/SafeExamBrowser.WindowsApi/Monitoring/KeyboardHook.cs @@ -8,7 +8,6 @@ using System; using System.Runtime.InteropServices; -using System.Threading; using SafeExamBrowser.Contracts.Monitoring; using SafeExamBrowser.WindowsApi.Constants; using SafeExamBrowser.WindowsApi.Delegates; @@ -25,15 +24,13 @@ namespace SafeExamBrowser.WindowsApi.Monitoring private const int DELETE = 46; private bool altPressed, ctrlPressed; - private HookDelegate hookProc; + private HookDelegate hookDelegate; internal IntPtr Handle { get; private set; } - internal AutoResetEvent InputEvent { get; private set; } internal IKeyboardInterceptor Interceptor { get; private set; } internal KeyboardHook(IKeyboardInterceptor interceptor) { - InputEvent = new AutoResetEvent(false); Interceptor = interceptor; } @@ -46,9 +43,9 @@ namespace SafeExamBrowser.WindowsApi.Monitoring // IMORTANT: // Ensures that the hook delegate does not get garbage collected prematurely, as it will be passed to unmanaged code. // Not doing so will result in a CallbackOnCollectedDelegate error and subsequent application crash! - hookProc = new HookDelegate(LowLevelKeyboardProc); + hookDelegate = new HookDelegate(LowLevelKeyboardProc); - Handle = User32.SetWindowsHookEx(HookType.WH_KEYBOARD_LL, hookProc, moduleHandle, 0); + Handle = User32.SetWindowsHookEx(HookType.WH_KEYBOARD_LL, hookDelegate, moduleHandle, 0); } internal bool Detach() @@ -58,8 +55,6 @@ namespace SafeExamBrowser.WindowsApi.Monitoring private IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam) { - InputEvent.Set(); - if (nCode >= 0) { var keyData = (KBDLLHOOKSTRUCT) Marshal.PtrToStructure(lParam, typeof(KBDLLHOOKSTRUCT)); diff --git a/SafeExamBrowser.WindowsApi/Monitoring/MouseHook.cs b/SafeExamBrowser.WindowsApi/Monitoring/MouseHook.cs index 28371f66..dd1f1701 100644 --- a/SafeExamBrowser.WindowsApi/Monitoring/MouseHook.cs +++ b/SafeExamBrowser.WindowsApi/Monitoring/MouseHook.cs @@ -8,7 +8,6 @@ using System; using System.Runtime.InteropServices; -using System.Threading; using SafeExamBrowser.Contracts.Monitoring; using SafeExamBrowser.WindowsApi.Constants; using SafeExamBrowser.WindowsApi.Delegates; @@ -18,15 +17,13 @@ namespace SafeExamBrowser.WindowsApi.Monitoring { internal class MouseHook { - private HookDelegate hookProc; + private HookDelegate hookDelegate; internal IntPtr Handle { get; private set; } - internal AutoResetEvent InputEvent { get; private set; } internal IMouseInterceptor Interceptor { get; private set; } internal MouseHook(IMouseInterceptor interceptor) { - InputEvent = new AutoResetEvent(false); Interceptor = interceptor; } @@ -39,9 +36,9 @@ namespace SafeExamBrowser.WindowsApi.Monitoring // IMORTANT: // Ensures that the hook delegate does not get garbage collected prematurely, as it will be passed to unmanaged code. // Not doing so will result in a CallbackOnCollectedDelegate error and subsequent application crash! - hookProc = new HookDelegate(LowLevelMouseProc); + hookDelegate = new HookDelegate(LowLevelMouseProc); - Handle = User32.SetWindowsHookEx(HookType.WH_MOUSE_LL, hookProc, moduleHandle, 0); + Handle = User32.SetWindowsHookEx(HookType.WH_MOUSE_LL, hookDelegate, moduleHandle, 0); } internal bool Detach() @@ -51,8 +48,6 @@ namespace SafeExamBrowser.WindowsApi.Monitoring private IntPtr LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam) { - InputEvent.Set(); - if (nCode >= 0 && !Ignore(wParam.ToInt32())) { var mouseData = (MSLLHOOKSTRUCT) Marshal.PtrToStructure(lParam, typeof(MSLLHOOKSTRUCT)); diff --git a/SafeExamBrowser.WindowsApi/Monitoring/SystemHook.cs b/SafeExamBrowser.WindowsApi/Monitoring/SystemHook.cs new file mode 100644 index 00000000..a8ae9719 --- /dev/null +++ b/SafeExamBrowser.WindowsApi/Monitoring/SystemHook.cs @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2018 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.Threading; +using SafeExamBrowser.Contracts.WindowsApi.Events; +using SafeExamBrowser.WindowsApi.Constants; +using SafeExamBrowser.WindowsApi.Delegates; + +namespace SafeExamBrowser.WindowsApi.Monitoring +{ + internal class SystemHook + { + private SystemEventCallback callback; + private AutoResetEvent detachEvent, detachResultAvailableEvent; + private bool detachSuccess; + private EventDelegate eventDelegate; + private uint eventId; + + internal IntPtr Handle { get; private set; } + internal Guid Id { get; private set; } + + public SystemHook(SystemEventCallback callback, uint eventId) + { + this.callback = callback; + this.detachEvent = new AutoResetEvent(false); + this.detachResultAvailableEvent = new AutoResetEvent(false); + this.eventId = eventId; + this.Id = Guid.NewGuid(); + } + + internal void Attach() + { + // IMORTANT: + // Ensures that the hook delegate does not get garbage collected prematurely, as it will be passed to unmanaged code. + // Not doing so will result in a CallbackOnCollectedDelegate error and subsequent application crash! + eventDelegate = new EventDelegate(LowLevelSystemProc); + + Handle = User32.SetWinEventHook(eventId, eventId, IntPtr.Zero, eventDelegate, 0, 0, Constant.WINEVENT_OUTOFCONTEXT); + } + + internal void AwaitDetach() + { + detachEvent.WaitOne(); + detachSuccess = User32.UnhookWinEvent(Handle); + detachResultAvailableEvent.Set(); + } + + internal bool Detach() + { + detachEvent.Set(); + detachResultAvailableEvent.WaitOne(); + + return detachSuccess; + } + + private void LowLevelSystemProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime) + { + callback(hwnd); + } + } +} diff --git a/SafeExamBrowser.WindowsApi/NativeMethods.cs b/SafeExamBrowser.WindowsApi/NativeMethods.cs index 744efa78..2787c443 100644 --- a/SafeExamBrowser.WindowsApi/NativeMethods.cs +++ b/SafeExamBrowser.WindowsApi/NativeMethods.cs @@ -16,8 +16,8 @@ using System.Text; using System.Threading; using SafeExamBrowser.Contracts.Monitoring; using SafeExamBrowser.Contracts.WindowsApi; +using SafeExamBrowser.Contracts.WindowsApi.Events; using SafeExamBrowser.WindowsApi.Constants; -using SafeExamBrowser.WindowsApi.Delegates; using SafeExamBrowser.WindowsApi.Monitoring; using SafeExamBrowser.WindowsApi.Types; @@ -25,28 +25,28 @@ namespace SafeExamBrowser.WindowsApi { public class NativeMethods : INativeMethods { - private ConcurrentDictionary EventDelegates = new ConcurrentDictionary(); private ConcurrentDictionary KeyboardHooks = new ConcurrentDictionary(); private ConcurrentDictionary MouseHooks = new ConcurrentDictionary(); + private ConcurrentDictionary SystemHooks = new ConcurrentDictionary(); /// /// Upon finalization, unregister all active system events and hooks... /// ~NativeMethods() { - foreach (var handle in EventDelegates.Keys) + foreach (var hook in SystemHooks.Values) { - User32.UnhookWinEvent(handle); + hook.Detach(); } - foreach (var handle in KeyboardHooks.Keys) + foreach (var hook in KeyboardHooks.Values) { - User32.UnhookWindowsHookEx(handle); + hook.Detach(); } - foreach (var handle in MouseHooks.Keys) + foreach (var hook in MouseHooks.Values) { - User32.UnhookWindowsHookEx(handle); + hook.Detach(); } } @@ -63,7 +63,7 @@ namespace SafeExamBrowser.WindowsApi throw new Win32Exception(Marshal.GetLastWin32Error()); } - KeyboardHooks.TryRemove(hook.Handle, out KeyboardHook h); + KeyboardHooks.TryRemove(hook.Handle, out _); } } @@ -80,20 +80,25 @@ namespace SafeExamBrowser.WindowsApi throw new Win32Exception(Marshal.GetLastWin32Error()); } - MouseHooks.TryRemove(hook.Handle, out MouseHook h); + MouseHooks.TryRemove(hook.Handle, out _); } } - public void DeregisterSystemEvent(IntPtr handle) + public void DeregisterSystemEventHook(Guid hookId) { - var success = User32.UnhookWinEvent(handle); + var hook = SystemHooks.Values.FirstOrDefault(h => h.Id == hookId); - if (!success) + if (hook != null) { - throw new Win32Exception(Marshal.GetLastWin32Error()); - } + var success = hook.Detach(); - EventDelegates.TryRemove(handle, out EventDelegate d); + if (!success) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + SystemHooks.TryRemove(hook.Handle, out _); + } } public void EmptyClipboard() @@ -236,6 +241,7 @@ namespace SafeExamBrowser.WindowsApi var hookThread = new Thread(() => { var hook = new KeyboardHook(interceptor); + var sleepEvent = new AutoResetEvent(false); hook.Attach(); KeyboardHooks[hook.Handle] = hook; @@ -243,7 +249,7 @@ namespace SafeExamBrowser.WindowsApi while (true) { - hook.InputEvent.WaitOne(); + sleepEvent.WaitOne(); } }); @@ -260,6 +266,7 @@ namespace SafeExamBrowser.WindowsApi var hookThread = new Thread(() => { var hook = new MouseHook(interceptor); + var sleepEvent = new AutoResetEvent(false); hook.Attach(); MouseHooks[hook.Handle] = hook; @@ -267,7 +274,7 @@ namespace SafeExamBrowser.WindowsApi while (true) { - hook.InputEvent.WaitOne(); + sleepEvent.WaitOne(); } }); @@ -278,38 +285,38 @@ namespace SafeExamBrowser.WindowsApi hookReadyEvent.WaitOne(); } - public IntPtr RegisterSystemForegroundEvent(Action callback) + public Guid RegisterSystemCaptureStartEvent(SystemEventCallback callback) { - void evenDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime) - { - callback(hwnd); - } - - var handle = User32.SetWinEventHook(Constant.EVENT_SYSTEM_FOREGROUND, Constant.EVENT_SYSTEM_FOREGROUND, IntPtr.Zero, evenDelegate, 0, 0, Constant.WINEVENT_OUTOFCONTEXT); - - // IMORTANT: - // Ensures that the event delegate does not get garbage collected prematurely, as it will be passed to unmanaged code. - // Not doing so will result in a CallbackOnCollectedDelegate error and subsequent application crash! - EventDelegates[handle] = evenDelegate; - - return handle; + return RegisterSystemEvent(callback, Constant.EVENT_SYSTEM_CAPTURESTART); } - public IntPtr RegisterSystemCaptureStartEvent(Action callback) + public Guid RegisterSystemForegroundEvent(SystemEventCallback callback) { - void eventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime) + return RegisterSystemEvent(callback, Constant.EVENT_SYSTEM_FOREGROUND); + } + + internal Guid RegisterSystemEvent(SystemEventCallback callback, uint eventId) + { + var hookId = default(Guid); + var hookReadyEvent = new AutoResetEvent(false); + var hookThread = new Thread(() => { - callback(hwnd); - } + var hook = new SystemHook(callback, eventId); - var handle = User32.SetWinEventHook(Constant.EVENT_SYSTEM_CAPTURESTART, Constant.EVENT_SYSTEM_CAPTURESTART, IntPtr.Zero, eventDelegate, 0, 0, Constant.WINEVENT_OUTOFCONTEXT); + hook.Attach(); + hookId = hook.Id; + SystemHooks[hook.Handle] = hook; + hookReadyEvent.Set(); + hook.AwaitDetach(); + }); - // IMORTANT: - // Ensures that the event delegate does not get garbage collected prematurely, as it will be passed to unmanaged code. - // Not doing so will result in a CallbackOnCollectedDelegate error and subsequent application crash! - EventDelegates[handle] = eventDelegate; + hookThread.SetApartmentState(ApartmentState.STA); + hookThread.IsBackground = true; + hookThread.Start(); - return handle; + hookReadyEvent.WaitOne(); + + return hookId; } public void RemoveWallpaper() diff --git a/SafeExamBrowser.WindowsApi/SafeExamBrowser.WindowsApi.csproj b/SafeExamBrowser.WindowsApi/SafeExamBrowser.WindowsApi.csproj index 8ff0a0e4..8b927bfb 100644 --- a/SafeExamBrowser.WindowsApi/SafeExamBrowser.WindowsApi.csproj +++ b/SafeExamBrowser.WindowsApi/SafeExamBrowser.WindowsApi.csproj @@ -62,6 +62,7 @@ +