SEBWIN-226: Implemented termination activator for global CTRL+Q keyboard shortcut.

This commit is contained in:
dbuechel 2019-03-29 07:46:21 +01:00
parent 25022295e1
commit 719411d8e3
20 changed files with 280 additions and 49 deletions

View file

@ -49,6 +49,7 @@ namespace SafeExamBrowser.Client.UnitTests
private Settings settings;
private Mock<Action> shutdown;
private Mock<ITaskbar> taskbar;
private Mock<ITerminationActivator> terminationActivator;
private Mock<IText> text;
private Mock<IUserInterfaceFactory> uiFactory;
private Mock<IWindowMonitor> windowMonitor;
@ -74,6 +75,7 @@ namespace SafeExamBrowser.Client.UnitTests
settings = new Settings();
shutdown = new Mock<Action>();
taskbar = new Mock<ITaskbar>();
terminationActivator = new Mock<ITerminationActivator>();
text = new Mock<IText>();
uiFactory = new Mock<IUserInterfaceFactory>();
windowMonitor = new Mock<IWindowMonitor>();
@ -94,6 +96,7 @@ namespace SafeExamBrowser.Client.UnitTests
runtimeProxy.Object,
shutdown.Object,
taskbar.Object,
terminationActivator.Object,
text.Object,
uiFactory.Object,
windowMonitor.Object);

View file

@ -17,6 +17,7 @@ using SafeExamBrowser.Contracts.Logging;
using SafeExamBrowser.Contracts.SystemComponents;
using SafeExamBrowser.Contracts.UserInterface;
using SafeExamBrowser.Contracts.UserInterface.Shell;
using SafeExamBrowser.Contracts.WindowsApi;
namespace SafeExamBrowser.Client.UnitTests.Operations
{
@ -28,6 +29,7 @@ namespace SafeExamBrowser.Client.UnitTests.Operations
private ActionCenterSettings actionCenterSettings;
private Mock<ILogger> logger;
private TaskbarSettings taskbarSettings;
private Mock<ITerminationActivator> terminationActivator;
private Mock<INotificationInfo> aboutInfo;
private Mock<INotificationController> aboutController;
private Mock<INotificationInfo> logInfo;
@ -59,6 +61,7 @@ namespace SafeExamBrowser.Client.UnitTests.Operations
systemInfo = new Mock<ISystemInfo>();
taskbar = new Mock<ITaskbar>();
taskbarSettings = new TaskbarSettings();
terminationActivator = new Mock<ITerminationActivator>();
text = new Mock<IText>();
uiFactory = new Mock<IUserInterfaceFactory>();
@ -84,6 +87,7 @@ namespace SafeExamBrowser.Client.UnitTests.Operations
systemInfo.Object,
taskbar.Object,
taskbarSettings,
terminationActivator.Object,
text.Object,
uiFactory.Object);
}

View file

@ -44,6 +44,7 @@ namespace SafeExamBrowser.Client
private Action shutdown;
private ISplashScreen splashScreen;
private ITaskbar taskbar;
private ITerminationActivator terminationActivator;
private IText text;
private IUserInterfaceFactory uiFactory;
private IWindowMonitor windowMonitor;
@ -79,6 +80,7 @@ namespace SafeExamBrowser.Client
IRuntimeProxy runtime,
Action shutdown,
ITaskbar taskbar,
ITerminationActivator terminationActivator,
IText text,
IUserInterfaceFactory uiFactory,
IWindowMonitor windowMonitor)
@ -94,6 +96,7 @@ namespace SafeExamBrowser.Client
this.runtime = runtime;
this.shutdown = shutdown;
this.taskbar = taskbar;
this.terminationActivator = terminationActivator;
this.text = text;
this.uiFactory = uiFactory;
this.windowMonitor = windowMonitor;
@ -178,6 +181,7 @@ namespace SafeExamBrowser.Client
processMonitor.ExplorerStarted += ProcessMonitor_ExplorerStarted;
runtime.ConnectionLost += Runtime_ConnectionLost;
taskbar.QuitButtonClicked += Shell_QuitButtonClicked;
terminationActivator.Activated += TerminationActivator_Activated;
windowMonitor.WindowChanged += WindowMonitor_WindowChanged;
}
@ -188,6 +192,7 @@ namespace SafeExamBrowser.Client
processMonitor.ExplorerStarted -= ProcessMonitor_ExplorerStarted;
runtime.ConnectionLost -= Runtime_ConnectionLost;
taskbar.QuitButtonClicked -= Shell_QuitButtonClicked;
terminationActivator.Activated -= TerminationActivator_Activated;
windowMonitor.WindowChanged -= WindowMonitor_WindowChanged;
if (Browser != null)
@ -383,6 +388,35 @@ namespace SafeExamBrowser.Client
}
private void Shell_QuitButtonClicked(System.ComponentModel.CancelEventArgs args)
{
terminationActivator.Pause();
args.Cancel = !TryInitiateShutdown();
terminationActivator.Resume();
}
private void TerminationActivator_Activated()
{
terminationActivator.Pause();
TryInitiateShutdown();
terminationActivator.Resume();
}
private void WindowMonitor_WindowChanged(IntPtr window)
{
var allowed = processMonitor.BelongsToAllowedProcess(window);
if (!allowed)
{
var success = windowMonitor.Hide(window);
if (!success)
{
windowMonitor.Close(window);
}
}
}
private bool TryInitiateShutdown()
{
var hasQuitPassword = !String.IsNullOrEmpty(Settings.QuitPasswordHash);
var requestShutdown = false;
@ -400,31 +434,18 @@ namespace SafeExamBrowser.Client
{
var communication = runtime.RequestShutdown();
if (!communication.Success)
if (communication.Success)
{
return true;
}
else
{
logger.Error("Failed to communicate shutdown request to the runtime!");
messageBox.Show(TextKey.MessageBox_QuitError, TextKey.MessageBox_QuitErrorTitle, icon: MessageBoxIcon.Error);
}
}
else
{
args.Cancel = true;
}
}
private void WindowMonitor_WindowChanged(IntPtr window)
{
var allowed = processMonitor.BelongsToAllowedProcess(window);
if (!allowed)
{
var success = windowMonitor.Hide(window);
if (!success)
{
windowMonitor.Close(window);
}
}
return false;
}
private bool TryConfirmShutdown()

View file

@ -71,6 +71,7 @@ namespace SafeExamBrowser.Client
private ISystemComponent<ISystemWirelessNetworkControl> wirelessNetwork;
private ISystemInfo systemInfo;
private ITaskbar taskbar;
private ITerminationActivator terminationActivator;
private IText text;
private ITextResource textResource;
private IUserInterfaceFactory uiFactory;
@ -98,6 +99,7 @@ namespace SafeExamBrowser.Client
uiFactory = BuildUserInterfaceFactory();
runtimeProxy = new RuntimeProxy(runtimeHostUri, new ProxyObjectFactory(), new ModuleLogger(logger, nameof(RuntimeProxy)));
taskbar = BuildTaskbar();
terminationActivator = new TerminationActivator(new ModuleLogger(logger, nameof(TerminationActivator)));
windowMonitor = new WindowMonitor(new ModuleLogger(logger, nameof(WindowMonitor)), nativeMethods);
wirelessNetwork = new WirelessNetwork(new ModuleLogger(logger, nameof(WirelessNetwork)), text);
@ -137,6 +139,7 @@ namespace SafeExamBrowser.Client
runtimeProxy,
shutdown,
taskbar,
terminationActivator,
text,
uiFactory,
windowMonitor);
@ -279,6 +282,7 @@ namespace SafeExamBrowser.Client
systemInfo,
taskbar,
configuration.Settings.Taskbar,
terminationActivator,
text,
uiFactory);

View file

@ -16,6 +16,7 @@ using SafeExamBrowser.Contracts.Logging;
using SafeExamBrowser.Contracts.SystemComponents;
using SafeExamBrowser.Contracts.UserInterface;
using SafeExamBrowser.Contracts.UserInterface.Shell;
using SafeExamBrowser.Contracts.WindowsApi;
namespace SafeExamBrowser.Client.Operations
{
@ -35,6 +36,7 @@ namespace SafeExamBrowser.Client.Operations
private ISystemInfo systemInfo;
private ITaskbar taskbar;
private TaskbarSettings taskbarSettings;
private ITerminationActivator terminationActivator;
private IText text;
private IUserInterfaceFactory uiFactory;
@ -56,6 +58,7 @@ namespace SafeExamBrowser.Client.Operations
ISystemInfo systemInfo,
ITaskbar taskbar,
TaskbarSettings taskbarSettings,
ITerminationActivator terminationActivator,
IText text,
IUserInterfaceFactory uiFactory)
{
@ -71,6 +74,7 @@ namespace SafeExamBrowser.Client.Operations
this.powerSupply = powerSupply;
this.systemInfo = systemInfo;
this.taskbarSettings = taskbarSettings;
this.terminationActivator = terminationActivator;
this.text = text;
this.taskbar = taskbar;
this.uiFactory = uiFactory;
@ -85,6 +89,7 @@ namespace SafeExamBrowser.Client.Operations
InitializeSystemComponents();
InitializeActionCenter();
InitializeTaskbar();
InitializeActivators();
return OperationResult.Success;
}
@ -101,6 +106,11 @@ namespace SafeExamBrowser.Client.Operations
return OperationResult.Success;
}
private void InitializeActivators()
{
terminationActivator.Start();
}
private void InitializeActionCenter()
{
if (actionCenterSettings.EnableActionCenter)
@ -270,6 +280,8 @@ namespace SafeExamBrowser.Client.Operations
private void TerminateActivators()
{
terminationActivator.Stop();
if (actionCenterSettings.EnableActionCenter)
{
foreach (var activator in activators)

View file

@ -218,6 +218,7 @@
<Compile Include="UserInterface\MessageBox\MessageBoxIcon.cs" />
<Compile Include="WindowsApi\Events\ProcessTerminatedEventHandler.cs" />
<Compile Include="WindowsApi\Events\SystemEventCallback.cs" />
<Compile Include="WindowsApi\Events\TerminationActivatorEventHandler.cs" />
<Compile Include="WindowsApi\IBounds.cs" />
<Compile Include="WindowsApi\IDesktop.cs" />
<Compile Include="WindowsApi\IDesktopFactory.cs" />
@ -225,6 +226,7 @@
<Compile Include="WindowsApi\INativeMethods.cs" />
<Compile Include="WindowsApi\IProcess.cs" />
<Compile Include="WindowsApi\IProcessFactory.cs" />
<Compile Include="WindowsApi\ITerminationActivator.cs" />
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

View file

@ -18,17 +18,17 @@ namespace SafeExamBrowser.Contracts.UserInterface.Shell
/// <summary>
/// Fired when the action center should be made visible.
/// </summary>
event ActivatorEventHandler Activate;
event ActivatorEventHandler Activated;
/// <summary>
/// Fired when the action center should be made invisible.
/// </summary>
event ActivatorEventHandler Deactivate;
event ActivatorEventHandler Deactivated;
/// <summary>
/// Fired when the action center visibility should be toggled.
/// </summary>
event ActivatorEventHandler Toggle;
event ActivatorEventHandler Toggled;
/// <summary>
/// Starts monitoring user input events.

View file

@ -0,0 +1,15 @@
/*
* Copyright (c) 2019 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.WindowsApi.Events
{
/// <summary>
/// Event handler fired by the <see cref="ITerminationActivator"/> when the user would like to terminate the application.
/// </summary>
public delegate void TerminationActivatorEventHandler();
}

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2019 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 SafeExamBrowser.Contracts.WindowsApi.Events;
namespace SafeExamBrowser.Contracts.WindowsApi
{
/// <summary>
/// A module which observes user input and indicates when the user would like to terminate the application.
/// </summary>
public interface ITerminationActivator
{
/// <summary>
/// Fired when a termination request has been detected.
/// </summary>
event TerminationActivatorEventHandler Activated;
/// <summary>
/// Temporarily stops processing all user input.
/// </summary>
void Pause();
/// <summary>
/// Resumes processing user input.
/// </summary>
void Resume();
/// <summary>
/// Starts monitoring user input events.
/// </summary>
void Start();
/// <summary>
/// Stops monitoring user input events.
/// </summary>
void Stop();
}
}

View file

@ -82,9 +82,9 @@ namespace SafeExamBrowser.UserInterface.Desktop
public void Register(IActionCenterActivator activator)
{
activator.Activate += Activator_Activate;
activator.Deactivate += Activator_Deactivate;
activator.Toggle += Activator_Toggle;
activator.Activated += Activator_Activated;
activator.Deactivated += Activator_Deactivated;
activator.Toggled += Activator_Toggled;
}
public new void Show()
@ -150,7 +150,7 @@ namespace SafeExamBrowser.UserInterface.Desktop
HideAnimated();
}
private void Activator_Activate()
private void Activator_Activated()
{
Dispatcher.InvokeAsync(() =>
{
@ -161,7 +161,7 @@ namespace SafeExamBrowser.UserInterface.Desktop
});
}
private void Activator_Deactivate()
private void Activator_Deactivated()
{
Dispatcher.InvokeAsync(() =>
{
@ -172,7 +172,7 @@ namespace SafeExamBrowser.UserInterface.Desktop
});
}
private void Activator_Toggle()
private void Activator_Toggled()
{
Dispatcher.InvokeAsync(() =>
{

View file

@ -82,9 +82,9 @@ namespace SafeExamBrowser.UserInterface.Mobile
public void Register(IActionCenterActivator activator)
{
activator.Activate += Activator_Activate;
activator.Deactivate += Activator_Deactivate;
activator.Toggle += Activator_Toggle;
activator.Activated += Activator_Activated;
activator.Deactivated += Activator_Deactivated;
activator.Toggled += Activator_Toggled;
}
public new void Show()
@ -150,7 +150,7 @@ namespace SafeExamBrowser.UserInterface.Mobile
HideAnimated();
}
private void Activator_Activate()
private void Activator_Activated()
{
Dispatcher.InvokeAsync(() =>
{
@ -161,7 +161,7 @@ namespace SafeExamBrowser.UserInterface.Mobile
});
}
private void Activator_Deactivate()
private void Activator_Deactivated()
{
Dispatcher.InvokeAsync(() =>
{
@ -172,7 +172,7 @@ namespace SafeExamBrowser.UserInterface.Mobile
});
}
private void Activator_Toggle()
private void Activator_Toggled()
{
Dispatcher.InvokeAsync(() =>
{

View file

@ -14,6 +14,7 @@ namespace SafeExamBrowser.WindowsApi.Constants
internal enum VirtualKeyCode
{
A = 0x41,
Q = 0x51,
Delete = 0x2E,
LeftAlt = 0xA4,
LeftControl = 0xA2,

View file

@ -13,7 +13,7 @@ using SafeExamBrowser.WindowsApi.Constants;
using SafeExamBrowser.WindowsApi.Delegates;
using SafeExamBrowser.WindowsApi.Types;
namespace SafeExamBrowser.WindowsApi.Monitoring
namespace SafeExamBrowser.WindowsApi.Hooks
{
internal class KeyboardHook
{

View file

@ -13,7 +13,7 @@ using SafeExamBrowser.WindowsApi.Constants;
using SafeExamBrowser.WindowsApi.Delegates;
using SafeExamBrowser.WindowsApi.Types;
namespace SafeExamBrowser.WindowsApi.Monitoring
namespace SafeExamBrowser.WindowsApi.Hooks
{
internal class MouseHook
{

View file

@ -12,7 +12,7 @@ using SafeExamBrowser.Contracts.WindowsApi.Events;
using SafeExamBrowser.WindowsApi.Constants;
using SafeExamBrowser.WindowsApi.Delegates;
namespace SafeExamBrowser.WindowsApi.Monitoring
namespace SafeExamBrowser.WindowsApi.Hooks
{
internal class SystemHook
{

View file

@ -25,9 +25,9 @@ namespace SafeExamBrowser.WindowsApi
private HookDelegate hookDelegate;
private ILogger logger;
public event ActivatorEventHandler Activate { add { } remove { } }
public event ActivatorEventHandler Deactivate { add { } remove { } }
public event ActivatorEventHandler Toggle;
public event ActivatorEventHandler Activated { add { } remove { } }
public event ActivatorEventHandler Deactivated { add { } remove { } }
public event ActivatorEventHandler Toggled;
public KeyboardActivator(ILogger logger)
{
@ -93,7 +93,7 @@ namespace SafeExamBrowser.WindowsApi
if (A && LeftWindows && changed)
{
logger.Debug("Detected toggle sequence for action center.");
Toggle?.Invoke();
Toggled?.Invoke();
return (IntPtr) 1;
}

View file

@ -18,7 +18,7 @@ using SafeExamBrowser.Contracts.Monitoring;
using SafeExamBrowser.Contracts.WindowsApi;
using SafeExamBrowser.Contracts.WindowsApi.Events;
using SafeExamBrowser.WindowsApi.Constants;
using SafeExamBrowser.WindowsApi.Monitoring;
using SafeExamBrowser.WindowsApi.Hooks;
using SafeExamBrowser.WindowsApi.Types;
namespace SafeExamBrowser.WindowsApi

View file

@ -64,8 +64,9 @@
<Compile Include="DesktopFactory.cs" />
<Compile Include="ExplorerShell.cs" />
<Compile Include="KeyboardActivator.cs" />
<Compile Include="Monitoring\MouseHook.cs" />
<Compile Include="Monitoring\SystemHook.cs" />
<Compile Include="Hooks\MouseHook.cs" />
<Compile Include="Hooks\SystemHook.cs" />
<Compile Include="TerminationActivator.cs" />
<Compile Include="Process.cs" />
<Compile Include="ProcessFactory.cs" />
<Compile Include="Constants\AccessMask.cs" />
@ -77,7 +78,7 @@
<Compile Include="Constants\ShowWindowCommand.cs" />
<Compile Include="Constants\SystemCommand.cs" />
<Compile Include="Kernel32.cs" />
<Compile Include="Monitoring\KeyboardHook.cs" />
<Compile Include="Hooks\KeyboardHook.cs" />
<Compile Include="NativeMethods.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Constants\SPI.cs" />

View file

@ -0,0 +1,125 @@
/*
* Copyright (c) 2019 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.Runtime.InteropServices;
using System.Threading;
using SafeExamBrowser.Contracts.Logging;
using SafeExamBrowser.Contracts.WindowsApi;
using SafeExamBrowser.Contracts.WindowsApi.Events;
using SafeExamBrowser.WindowsApi.Constants;
using SafeExamBrowser.WindowsApi.Delegates;
using SafeExamBrowser.WindowsApi.Types;
namespace SafeExamBrowser.WindowsApi
{
public class TerminationActivator : ITerminationActivator
{
private bool Q, LeftCtrl, RightCtrl, paused;
private IntPtr handle;
private HookDelegate hookDelegate;
private ILogger logger;
public event TerminationActivatorEventHandler Activated;
public TerminationActivator(ILogger logger)
{
this.logger = logger;
}
public void Pause()
{
paused = true;
}
public void Resume()
{
Q = false;
LeftCtrl = false;
RightCtrl = false;
paused = false;
}
public void Start()
{
var hookReadyEvent = new AutoResetEvent(false);
var hookThread = new Thread(() =>
{
var sleepEvent = new AutoResetEvent(false);
var process = System.Diagnostics.Process.GetCurrentProcess();
var module = process.MainModule;
var moduleHandle = Kernel32.GetModuleHandle(module.ModuleName);
// IMPORTANT:
// 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!
hookDelegate = new HookDelegate(LowLevelKeyboardProc);
handle = User32.SetWindowsHookEx(HookType.WH_KEYBOARD_LL, hookDelegate, moduleHandle, 0);
hookReadyEvent.Set();
while (true)
{
sleepEvent.WaitOne();
}
});
hookThread.SetApartmentState(ApartmentState.STA);
hookThread.IsBackground = true;
hookThread.Start();
hookReadyEvent.WaitOne();
}
public void Stop()
{
User32.UnhookWindowsHookEx(handle);
}
private IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0 && !paused)
{
var changed = false;
var keyData = (KBDLLHOOKSTRUCT) Marshal.PtrToStructure(lParam, typeof(KBDLLHOOKSTRUCT));
var pressed = IsPressed(wParam.ToInt32());
switch (keyData.KeyCode)
{
case (uint) VirtualKeyCode.Q:
changed = Q != pressed;
Q = pressed;
break;
case (uint) VirtualKeyCode.LeftControl:
changed = LeftCtrl != pressed;
LeftCtrl = pressed;
break;
case (uint) VirtualKeyCode.RightControl:
changed = RightCtrl != pressed;
RightCtrl = pressed;
break;
}
if (Q && (LeftCtrl || RightCtrl) && changed)
{
logger.Debug("Detected termination sequence.");
Activated?.Invoke();
return (IntPtr) 1;
}
}
return User32.CallNextHookEx(handle, nCode, wParam, lParam);
}
private bool IsPressed(int wParam)
{
return wParam == Constant.WM_KEYDOWN || wParam == Constant.WM_SYSKEYDOWN;
}
}
}

View file

@ -26,9 +26,9 @@ namespace SafeExamBrowser.WindowsApi
private bool isDown;
private ILogger logger;
public event ActivatorEventHandler Activate;
public event ActivatorEventHandler Deactivate { add { } remove { } }
public event ActivatorEventHandler Toggle { add { } remove { } }
public event ActivatorEventHandler Activated;
public event ActivatorEventHandler Deactivated { add { } remove { } }
public event ActivatorEventHandler Toggled { add { } remove { } }
public TouchActivator(ILogger logger)
{
@ -109,7 +109,7 @@ namespace SafeExamBrowser.WindowsApi
if (isDown && hasMoved)
{
logger.Debug("Detected activation gesture for action center.");
Activate?.Invoke();
Activated?.Invoke();
}
}