SEBWIN-220: Finally found a solution to run keyboard & mouse hooks in sepearate threads.

This commit is contained in:
dbuechel 2018-09-25 12:10:34 +02:00
parent b31ba1c2d5
commit b50c208f46
10 changed files with 87 additions and 37 deletions

View file

@ -96,16 +96,16 @@ namespace SafeExamBrowser.Client
operations.Enqueue(new RuntimeConnectionOperation(logger, runtimeProxy, startupToken)); operations.Enqueue(new RuntimeConnectionOperation(logger, runtimeProxy, startupToken));
operations.Enqueue(new ConfigurationOperation(configuration, logger, runtimeProxy)); operations.Enqueue(new ConfigurationOperation(configuration, logger, runtimeProxy));
operations.Enqueue(new DelegateOperation(UpdateAppConfig)); operations.Enqueue(new DelegateOperation(UpdateAppConfig));
operations.Enqueue(new DelayedInitializationOperation(BuildCommunicationHostOperation)); operations.Enqueue(new LazyInitializationOperation(BuildCommunicationHostOperation));
operations.Enqueue(new LazyInitializationOperation(BuildKeyboardInterceptorOperation));
// TODO // TODO
//operations.Enqueue(new DelayedInitializationOperation(BuildKeyboardInterceptorOperation));
//operations.Enqueue(new WindowMonitorOperation(logger, windowMonitor)); //operations.Enqueue(new WindowMonitorOperation(logger, windowMonitor));
operations.Enqueue(new DelayedInitializationOperation(BuildProcessMonitorOperation)); operations.Enqueue(new LazyInitializationOperation(BuildProcessMonitorOperation));
operations.Enqueue(new DisplayMonitorOperation(displayMonitor, logger, Taskbar)); operations.Enqueue(new DisplayMonitorOperation(displayMonitor, logger, Taskbar));
operations.Enqueue(new DelayedInitializationOperation(BuildTaskbarOperation)); operations.Enqueue(new LazyInitializationOperation(BuildTaskbarOperation));
operations.Enqueue(new DelayedInitializationOperation(BuildBrowserOperation)); operations.Enqueue(new LazyInitializationOperation(BuildBrowserOperation));
operations.Enqueue(new ClipboardOperation(logger, nativeMethods)); operations.Enqueue(new ClipboardOperation(logger, nativeMethods));
//operations.Enqueue(new DelayedInitializationOperation(BuildMouseInterceptorOperation)); operations.Enqueue(new LazyInitializationOperation(BuildMouseInterceptorOperation));
operations.Enqueue(new DelegateOperation(UpdateClientControllerDependencies)); operations.Enqueue(new DelegateOperation(UpdateClientControllerDependencies));
var sequence = new OperationSequence(logger, operations); var sequence = new OperationSequence(logger, operations);

View file

@ -16,7 +16,7 @@ using SafeExamBrowser.Core.Operations;
namespace SafeExamBrowser.Core.UnitTests.Operations namespace SafeExamBrowser.Core.UnitTests.Operations
{ {
[TestClass] [TestClass]
public class DelayedInitializationOperationTests public class LazyInitializationOperationTests
{ {
private Mock<IOperation> operationMock; private Mock<IOperation> operationMock;
@ -37,7 +37,7 @@ namespace SafeExamBrowser.Core.UnitTests.Operations
return operationMock.Object; return operationMock.Object;
}; };
var sut = new DelayedInitializationOperation(initialize); var sut = new LazyInitializationOperation(initialize);
sut.Perform(); sut.Perform();
@ -53,7 +53,7 @@ namespace SafeExamBrowser.Core.UnitTests.Operations
return operationMock.Object; return operationMock.Object;
}; };
var sut = new DelayedInitializationOperation(initialize); var sut = new LazyInitializationOperation(initialize);
sut.Repeat(); sut.Repeat();
} }
@ -67,7 +67,7 @@ namespace SafeExamBrowser.Core.UnitTests.Operations
return operationMock.Object; return operationMock.Object;
}; };
var sut = new DelayedInitializationOperation(initialize); var sut = new LazyInitializationOperation(initialize);
sut.Revert(); sut.Revert();
} }
@ -83,7 +83,7 @@ namespace SafeExamBrowser.Core.UnitTests.Operations
operationMock.Setup(o => o.Perform()).Returns(OperationResult.Success); operationMock.Setup(o => o.Perform()).Returns(OperationResult.Success);
operationMock.Setup(o => o.Repeat()).Returns(OperationResult.Failed); operationMock.Setup(o => o.Repeat()).Returns(OperationResult.Failed);
var sut = new DelayedInitializationOperation(initialize); var sut = new LazyInitializationOperation(initialize);
var perform = sut.Perform(); var perform = sut.Perform();
var repeat = sut.Repeat(); var repeat = sut.Repeat();
@ -101,7 +101,7 @@ namespace SafeExamBrowser.Core.UnitTests.Operations
return operationMock.Object; return operationMock.Object;
}; };
var sut = new DelayedInitializationOperation(initialize) var sut = new LazyInitializationOperation(initialize)
{ {
ProgressIndicator = new Mock<IProgressIndicator>().Object ProgressIndicator = new Mock<IProgressIndicator>().Object
}; };
@ -128,7 +128,7 @@ namespace SafeExamBrowser.Core.UnitTests.Operations
return new Mock<IOperation>().Object; return new Mock<IOperation>().Object;
}; };
var sut = new DelayedInitializationOperation(initialize); var sut = new LazyInitializationOperation(initialize);
sut.Perform(); sut.Perform();
sut.Repeat(); sut.Repeat();

View file

@ -79,7 +79,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Operations\CommunicationOperationTests.cs" /> <Compile Include="Operations\CommunicationOperationTests.cs" />
<Compile Include="Operations\DelayedInitializationOperationTests.cs" /> <Compile Include="Operations\LazyInitializationOperationTests.cs" />
<Compile Include="Operations\I18nOperationTests.cs" /> <Compile Include="Operations\I18nOperationTests.cs" />
<Compile Include="Operations\DelegateOperationTests.cs" /> <Compile Include="Operations\DelegateOperationTests.cs" />
<Compile Include="OperationModel\OperationSequenceTests.cs" /> <Compile Include="OperationModel\OperationSequenceTests.cs" />

View file

@ -13,18 +13,18 @@ using SafeExamBrowser.Contracts.UserInterface;
namespace SafeExamBrowser.Core.Operations namespace SafeExamBrowser.Core.Operations
{ {
/// <summary> /// <summary>
/// A wrapper operation to allow for a delayed (just-in-time) instantiation of an operation. Is useful when e.g. dependencies for a /// A wrapper operation to allow for a lazy (just-in-time) instantiation of an operation, initialized on <see cref="Perform"/>.
/// certain operation are not available during execution of the composition root, but rather only after a preceding operation within /// Is useful when e.g. dependencies for a certain operation are not available during execution of the composition root, but rather
/// an <see cref="IOperationSequence"/> has finished. /// only after a preceding operation within an <see cref="IOperationSequence"/> has finished.
/// </summary> /// </summary>
public class DelayedInitializationOperation : IOperation public class LazyInitializationOperation : IOperation
{ {
private Func<IOperation> initialize; private Func<IOperation> initialize;
private IOperation operation; private IOperation operation;
public IProgressIndicator ProgressIndicator { get; set; } public IProgressIndicator ProgressIndicator { get; set; }
public DelayedInitializationOperation(Func<IOperation> initialize) public LazyInitializationOperation(Func<IOperation> initialize)
{ {
this.initialize = initialize; this.initialize = initialize;
} }

View file

@ -55,7 +55,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Operations\CommunicationOperation.cs" /> <Compile Include="Operations\CommunicationOperation.cs" />
<Compile Include="Operations\DelayedInitializationOperation.cs" /> <Compile Include="Operations\LazyInitializationOperation.cs" />
<Compile Include="Operations\I18nOperation.cs" /> <Compile Include="Operations\I18nOperation.cs" />
<Compile Include="Operations\DelegateOperation.cs" /> <Compile Include="Operations\DelegateOperation.cs" />
<Compile Include="OperationModel\OperationSequence.cs" /> <Compile Include="OperationModel\OperationSequence.cs" />

View file

@ -48,11 +48,11 @@ namespace SafeExamBrowser.WindowsApi
if (success) if (success)
{ {
logger.Debug($"Successfully resumed thread #{thread.Id} of explorer shell process."); logger.Debug($"Successfully resumed explorer shell thread with ID = {thread.Id}.");
} }
else else
{ {
logger.Warn($"Failed to resume thread #{thread.Id} of explorer shell process within {MAX_ATTEMPTS} attempts!"); logger.Warn($"Failed to resume explorer shell thread with ID = {thread.Id} within {MAX_ATTEMPTS} attempts!");
} }
} }
} }
@ -89,7 +89,7 @@ namespace SafeExamBrowser.WindowsApi
if (shellProcess != null) if (shellProcess != null)
{ {
logger.Debug($"Found explorer shell processes with PID = {processId}."); logger.Debug($"Found explorer shell processes with PID = {processId} and {shellProcess.Threads.Count} threads.");
foreach (ProcessThread thread in shellProcess.Threads) foreach (ProcessThread thread in shellProcess.Threads)
{ {
@ -98,11 +98,11 @@ namespace SafeExamBrowser.WindowsApi
if (success) if (success)
{ {
suspendedThreads.Add(thread); suspendedThreads.Add(thread);
logger.Debug($"Successfully suspended thread #{thread.Id} of explorer shell process."); logger.Debug($"Successfully suspended explorer shell thread with ID = {thread.Id}.");
} }
else else
{ {
logger.Warn($"Failed to suspend thread #{thread.Id} of explorer shell process!"); logger.Warn($"Failed to suspend explorer shell thread with ID = {thread.Id}!");
} }
} }

View file

@ -8,6 +8,7 @@
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading;
using SafeExamBrowser.Contracts.Monitoring; using SafeExamBrowser.Contracts.Monitoring;
using SafeExamBrowser.WindowsApi.Constants; using SafeExamBrowser.WindowsApi.Constants;
using SafeExamBrowser.WindowsApi.Delegates; using SafeExamBrowser.WindowsApi.Delegates;
@ -27,23 +28,27 @@ namespace SafeExamBrowser.WindowsApi.Monitoring
private HookDelegate hookProc; private HookDelegate hookProc;
internal IntPtr Handle { get; private set; } internal IntPtr Handle { get; private set; }
internal AutoResetEvent InputEvent { get; private set; }
internal IKeyboardInterceptor Interceptor { get; private set; } internal IKeyboardInterceptor Interceptor { get; private set; }
internal KeyboardHook(IKeyboardInterceptor interceptor) internal KeyboardHook(IKeyboardInterceptor interceptor)
{ {
InputEvent = new AutoResetEvent(false);
Interceptor = interceptor; Interceptor = interceptor;
} }
internal void Attach() internal void Attach()
{ {
var module = Kernel32.GetModuleHandle(null); var process = System.Diagnostics.Process.GetCurrentProcess();
var module = process.MainModule;
var moduleHandle = Kernel32.GetModuleHandle(module.ModuleName);
// IMORTANT: // IMORTANT:
// Ensures that the hook delegate does not get garbage collected prematurely, as it will be passed to unmanaged code. // 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! // Not doing so will result in a <c>CallbackOnCollectedDelegate</c> error and subsequent application crash!
hookProc = new HookDelegate(LowLevelKeyboardProc); hookProc = new HookDelegate(LowLevelKeyboardProc);
Handle = User32.SetWindowsHookEx(HookType.WH_KEYBOARD_LL, hookProc, module, 0); Handle = User32.SetWindowsHookEx(HookType.WH_KEYBOARD_LL, hookProc, moduleHandle, 0);
} }
internal bool Detach() internal bool Detach()
@ -53,6 +58,8 @@ namespace SafeExamBrowser.WindowsApi.Monitoring
private IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam) private IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam)
{ {
InputEvent.Set();
if (nCode >= 0) if (nCode >= 0)
{ {
var keyData = (KBDLLHOOKSTRUCT) Marshal.PtrToStructure(lParam, typeof(KBDLLHOOKSTRUCT)); var keyData = (KBDLLHOOKSTRUCT) Marshal.PtrToStructure(lParam, typeof(KBDLLHOOKSTRUCT));

View file

@ -8,6 +8,7 @@
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading;
using SafeExamBrowser.Contracts.Monitoring; using SafeExamBrowser.Contracts.Monitoring;
using SafeExamBrowser.WindowsApi.Constants; using SafeExamBrowser.WindowsApi.Constants;
using SafeExamBrowser.WindowsApi.Delegates; using SafeExamBrowser.WindowsApi.Delegates;
@ -20,23 +21,27 @@ namespace SafeExamBrowser.WindowsApi.Monitoring
private HookDelegate hookProc; private HookDelegate hookProc;
internal IntPtr Handle { get; private set; } internal IntPtr Handle { get; private set; }
internal AutoResetEvent InputEvent { get; private set; }
internal IMouseInterceptor Interceptor { get; private set; } internal IMouseInterceptor Interceptor { get; private set; }
internal MouseHook(IMouseInterceptor interceptor) internal MouseHook(IMouseInterceptor interceptor)
{ {
InputEvent = new AutoResetEvent(false);
Interceptor = interceptor; Interceptor = interceptor;
} }
internal void Attach() internal void Attach()
{ {
var module = Kernel32.GetModuleHandle(null); var process = System.Diagnostics.Process.GetCurrentProcess();
var module = process.MainModule;
var moduleHandle = Kernel32.GetModuleHandle(module.ModuleName);
// IMORTANT: // IMORTANT:
// Ensures that the hook delegate does not get garbage collected prematurely, as it will be passed to unmanaged code. // 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! // Not doing so will result in a <c>CallbackOnCollectedDelegate</c> error and subsequent application crash!
hookProc = new HookDelegate(LowLevelMouseProc); hookProc = new HookDelegate(LowLevelMouseProc);
Handle = User32.SetWindowsHookEx(HookType.WH_MOUSE_LL, hookProc, module, 0); Handle = User32.SetWindowsHookEx(HookType.WH_MOUSE_LL, hookProc, moduleHandle, 0);
} }
internal bool Detach() internal bool Detach()
@ -46,6 +51,8 @@ namespace SafeExamBrowser.WindowsApi.Monitoring
private IntPtr LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam) private IntPtr LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam)
{ {
InputEvent.Set();
if (nCode >= 0 && !Ignore(wParam.ToInt32())) if (nCode >= 0 && !Ignore(wParam.ToInt32()))
{ {
var mouseData = (MSLLHOOKSTRUCT) Marshal.PtrToStructure(lParam, typeof(MSLLHOOKSTRUCT)); var mouseData = (MSLLHOOKSTRUCT) Marshal.PtrToStructure(lParam, typeof(MSLLHOOKSTRUCT));

View file

@ -13,6 +13,7 @@ using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Threading;
using SafeExamBrowser.Contracts.Monitoring; using SafeExamBrowser.Contracts.Monitoring;
using SafeExamBrowser.Contracts.WindowsApi; using SafeExamBrowser.Contracts.WindowsApi;
using SafeExamBrowser.WindowsApi.Constants; using SafeExamBrowser.WindowsApi.Constants;
@ -231,20 +232,50 @@ namespace SafeExamBrowser.WindowsApi
public void RegisterKeyboardHook(IKeyboardInterceptor interceptor) public void RegisterKeyboardHook(IKeyboardInterceptor interceptor)
{ {
var hook = new KeyboardHook(interceptor); var hookReadyEvent = new AutoResetEvent(false);
var hookThread = new Thread(() =>
{
var hook = new KeyboardHook(interceptor);
hook.Attach(); hook.Attach();
KeyboardHooks[hook.Handle] = hook;
hookReadyEvent.Set();
KeyboardHooks[hook.Handle] = hook; while (true)
{
hook.InputEvent.WaitOne();
}
});
hookThread.SetApartmentState(ApartmentState.STA);
hookThread.IsBackground = true;
hookThread.Start();
hookReadyEvent.WaitOne();
} }
public void RegisterMouseHook(IMouseInterceptor interceptor) public void RegisterMouseHook(IMouseInterceptor interceptor)
{ {
var hook = new MouseHook(interceptor); var hookReadyEvent = new AutoResetEvent(false);
var hookThread = new Thread(() =>
{
var hook = new MouseHook(interceptor);
hook.Attach(); hook.Attach();
MouseHooks[hook.Handle] = hook;
hookReadyEvent.Set();
MouseHooks[hook.Handle] = hook; while (true)
{
hook.InputEvent.WaitOne();
}
});
hookThread.SetApartmentState(ApartmentState.STA);
hookThread.IsBackground = true;
hookThread.Start();
hookReadyEvent.WaitOne();
} }
public IntPtr RegisterSystemForegroundEvent(Action<IntPtr> callback) public IntPtr RegisterSystemForegroundEvent(Action<IntPtr> callback)

View file

@ -24,7 +24,7 @@ namespace SafeExamBrowser.WindowsApi
public bool HasTerminated public bool HasTerminated
{ {
get { return process.HasExited; } get { process.Refresh(); return process.HasExited; }
} }
public Process(int id) public Process(int id)
@ -36,7 +36,12 @@ namespace SafeExamBrowser.WindowsApi
public void Kill() public void Kill()
{ {
process.Kill(); process.Refresh();
if (!process.HasExited)
{
process.Kill();
}
} }
private void Process_Exited(object sender, System.EventArgs e) private void Process_Exited(object sender, System.EventArgs e)