SEBWIN-220: Re-integrated system event hooks, now also running in separate threads. Decreased logging for EndpointNotFoundException in BaseProxy.

This commit is contained in:
dbuechel 2018-09-27 11:24:13 +02:00
parent b50c208f46
commit 67ba5fcce3
20 changed files with 308 additions and 121 deletions

View file

@ -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<ILogger> loggerMock;
private Mock<IWindowMonitor> windowMonitorMock;
private WindowMonitorOperation sut;
[TestInitialize]
public void Initialize()
{
loggerMock = new Mock<ILogger>();
windowMonitorMock = new Mock<IWindowMonitor>();
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();
}
}
}

View file

@ -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);
// }
//}
}
}
}

View file

@ -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;

View file

@ -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();
}
}
}
}

View file

@ -120,6 +120,24 @@ namespace SafeExamBrowser.Communication.UnitTests.Proxies
Assert.IsFalse(connected);
}
[TestMethod]
public void MustHandleMissingEndpointCorrectly()
{
var proxy = new Mock<IProxyObject>();
proxyObjectFactory.Setup(f => f.CreateObject(It.IsAny<string>())).Throws<EndpointNotFoundException>();
var token = Guid.NewGuid();
var connected = sut.Connect(token);
logger.Verify(l => l.Warn(It.IsAny<string>()), Times.AtLeastOnce());
logger.Verify(l => l.Error(It.IsAny<string>()), Times.Never());
logger.Verify(l => l.Error(It.IsAny<string>(), It.IsAny<Exception>()), Times.Never());
proxyObjectFactory.Verify(f => f.CreateObject(It.IsAny<string>()), Times.Once);
Assert.IsFalse(connected);
}
[TestMethod]
public void MustFailToDisconnectIfNotConnected()
{

View file

@ -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)
{

View file

@ -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)

View file

@ -181,6 +181,7 @@
<Compile Include="UserInterface\MessageBox\MessageBoxAction.cs" />
<Compile Include="UserInterface\MessageBox\MessageBoxIcon.cs" />
<Compile Include="WindowsApi\Events\ProcessTerminatedEventHandler.cs" />
<Compile Include="WindowsApi\Events\SystemEventCallback.cs" />
<Compile Include="WindowsApi\IBounds.cs" />
<Compile Include="WindowsApi\IDesktop.cs" />
<Compile Include="WindowsApi\IDesktopFactory.cs" />

View file

@ -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
{
/// <summary>
/// Defines the callback for system event hooks.
/// </summary>
public delegate void SystemEventCallback(IntPtr window);
}

View file

@ -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);
/// <summary>
/// Deregisters a previously registered system event.
/// Deregisters a previously registered system event hook.
/// </summary>
/// <exception cref="System.ComponentModel.Win32Exception">
/// If the event hook could not be successfully removed.
/// </exception>
void DeregisterSystemEvent(IntPtr handle);
void DeregisterSystemEventHook(Guid hookId);
/// <summary>
/// Empties the clipboard.
@ -128,17 +129,17 @@ namespace SafeExamBrowser.Contracts.WindowsApi
/// </summary>
void RegisterMouseHook(IMouseInterceptor interceptor);
/// <summary>
/// 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.
/// </summary>
Guid RegisterSystemCaptureStartEvent(SystemEventCallback callback);
/// <summary>
/// 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.
/// </summary>
IntPtr RegisterSystemForegroundEvent(Action<IntPtr> callback);
/// <summary>
/// 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.
/// </summary>
IntPtr RegisterSystemCaptureStartEvent(Action<IntPtr> callback);
Guid RegisterSystemForegroundEvent(SystemEventCallback callback);
/// <summary>
/// Removes the currently configured desktop wallpaper.

View file

@ -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}";
}
}
}

View file

@ -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<Window> minimizedWindows = new List<Window>();
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);
}
}
}
}

View file

@ -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();

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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 <c>CallbackOnCollectedDelegate</c> 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));

View file

@ -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 <c>CallbackOnCollectedDelegate</c> 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));

View file

@ -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 <c>CallbackOnCollectedDelegate</c> 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);
}
}
}

View file

@ -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<IntPtr, EventDelegate> EventDelegates = new ConcurrentDictionary<IntPtr, EventDelegate>();
private ConcurrentDictionary<IntPtr, KeyboardHook> KeyboardHooks = new ConcurrentDictionary<IntPtr, KeyboardHook>();
private ConcurrentDictionary<IntPtr, MouseHook> MouseHooks = new ConcurrentDictionary<IntPtr, MouseHook>();
private ConcurrentDictionary<IntPtr, SystemHook> SystemHooks = new ConcurrentDictionary<IntPtr, SystemHook>();
/// <summary>
/// Upon finalization, unregister all active system events and hooks...
/// </summary>
~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<IntPtr> 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 <c>CallbackOnCollectedDelegate</c> error and subsequent application crash!
EventDelegates[handle] = evenDelegate;
return handle;
return RegisterSystemEvent(callback, Constant.EVENT_SYSTEM_CAPTURESTART);
}
public IntPtr RegisterSystemCaptureStartEvent(Action<IntPtr> 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 <c>CallbackOnCollectedDelegate</c> 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()

View file

@ -62,6 +62,7 @@
<Compile Include="DesktopFactory.cs" />
<Compile Include="ExplorerShell.cs" />
<Compile Include="Monitoring\MouseHook.cs" />
<Compile Include="Monitoring\SystemHook.cs" />
<Compile Include="Process.cs" />
<Compile Include="ProcessFactory.cs" />
<Compile Include="Constants\ACCESS_MASK.cs" />