SEBWIN-220: Added functionality to suspend / resume the explorer shell process for kiosk mode Create New Desktop, since setting of the working area does not succeed as long as the shell is running.
This commit is contained in:
parent
d4295b3753
commit
bedfc5eac0
11 changed files with 205 additions and 13 deletions
|
@ -88,7 +88,7 @@ namespace SafeExamBrowser.Configuration
|
|||
|
||||
CurrentSettings = new Settings();
|
||||
|
||||
CurrentSettings.KioskMode = KioskMode.None;
|
||||
CurrentSettings.KioskMode = KioskMode.CreateNewDesktop;
|
||||
CurrentSettings.ServicePolicy = ServicePolicy.Optional;
|
||||
|
||||
CurrentSettings.Browser.StartUrl = "https://www.safeexambrowser.org/testing";
|
||||
|
|
|
@ -13,11 +13,21 @@ namespace SafeExamBrowser.Contracts.WindowsApi
|
|||
/// </summary>
|
||||
public interface IExplorerShell
|
||||
{
|
||||
/// <summary>
|
||||
/// Resumes the explorer shell process, if it was previously suspended.
|
||||
/// </summary>
|
||||
void Resume();
|
||||
|
||||
/// <summary>
|
||||
/// Starts the Windows explorer shell, if it isn't already running.
|
||||
/// </summary>
|
||||
void Start();
|
||||
|
||||
/// <summary>
|
||||
/// Suspends the explorer shell process, if it is running.
|
||||
/// </summary>
|
||||
void Suspend();
|
||||
|
||||
/// <summary>
|
||||
/// Gracefully terminates the Windows explorer shell, if it is running.
|
||||
/// </summary>
|
||||
|
|
|
@ -153,6 +153,12 @@ namespace SafeExamBrowser.Contracts.WindowsApi
|
|||
/// </summary>
|
||||
void RestoreWindow(IntPtr window);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to resume the thread referenced by the given thread ID. Returns <c>true</c> if the thread was successfully resumed,
|
||||
/// otherwise <c>false</c>.
|
||||
/// </summary>
|
||||
bool ResumeThread(int threadId);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a close message to the given window.
|
||||
/// </summary>
|
||||
|
@ -173,5 +179,11 @@ namespace SafeExamBrowser.Contracts.WindowsApi
|
|||
/// If the working area could not be set.
|
||||
/// </exception>
|
||||
void SetWorkingArea(IBounds bounds);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to suspend the thread referenced by the given thread ID. Returns <c>true</c> if the thread was successfully suspended,
|
||||
/// otherwise <c>false</c>.
|
||||
/// </summary>
|
||||
bool SuspendThread(int threadId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,6 +52,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
|
|||
var createNew = 0;
|
||||
var activate = 0;
|
||||
var setStartup = 0;
|
||||
var suspend = 0;
|
||||
|
||||
settings.KioskMode = KioskMode.CreateNewDesktop;
|
||||
|
||||
|
@ -59,6 +60,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
|
|||
desktopFactory.Setup(f => f.CreateNew(It.IsAny<string>())).Callback(() => createNew = ++order).Returns(newDesktop.Object);
|
||||
newDesktop.Setup(d => d.Activate()).Callback(() => activate = ++order);
|
||||
processFactory.SetupSet(f => f.StartupDesktop = It.IsAny<IDesktop>()).Callback(() => setStartup = ++order);
|
||||
explorerShell.Setup(s => s.Suspend()).Callback(() => suspend = ++order);
|
||||
|
||||
sut.Perform();
|
||||
|
||||
|
@ -66,11 +68,13 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
|
|||
desktopFactory.Verify(f => f.CreateNew(It.IsAny<string>()), Times.Once);
|
||||
newDesktop.Verify(d => d.Activate(), Times.Once);
|
||||
processFactory.VerifySet(f => f.StartupDesktop = newDesktop.Object, Times.Once);
|
||||
explorerShell.Verify(s => s.Suspend(), Times.Once);
|
||||
|
||||
Assert.AreEqual(1, getCurrrent);
|
||||
Assert.AreEqual(2, createNew);
|
||||
Assert.AreEqual(3, activate);
|
||||
Assert.AreEqual(4, setStartup);
|
||||
Assert.AreEqual(5, suspend);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
@ -92,6 +96,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
|
|||
var activate = 0;
|
||||
var setStartup = 0;
|
||||
var close = 0;
|
||||
var resume = 0;
|
||||
|
||||
settings.KioskMode = KioskMode.CreateNewDesktop;
|
||||
|
||||
|
@ -100,6 +105,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
|
|||
originalDesktop.Setup(d => d.Activate()).Callback(() => activate = ++order);
|
||||
processFactory.SetupSet(f => f.StartupDesktop = It.Is<IDesktop>(d => d == originalDesktop.Object)).Callback(() => setStartup = ++order);
|
||||
newDesktop.Setup(d => d.Close()).Callback(() => close = ++order);
|
||||
explorerShell.Setup(s => s.Resume()).Callback(() => resume = ++order);
|
||||
|
||||
sut.Perform();
|
||||
sut.Revert();
|
||||
|
@ -107,10 +113,12 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
|
|||
originalDesktop.Verify(d => d.Activate(), Times.Once);
|
||||
processFactory.VerifySet(f => f.StartupDesktop = originalDesktop.Object, Times.Once);
|
||||
newDesktop.Verify(d => d.Close(), Times.Once);
|
||||
explorerShell.Verify(s => s.Resume(), Times.Once);
|
||||
|
||||
Assert.AreEqual(1, activate);
|
||||
Assert.AreEqual(2, setStartup);
|
||||
Assert.AreEqual(3, close);
|
||||
Assert.AreEqual(4, resume);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
@ -138,6 +146,8 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
|
|||
|
||||
explorerShell.Verify(s => s.Terminate(), Times.Never);
|
||||
explorerShell.Verify(s => s.Start(), Times.Never);
|
||||
explorerShell.Verify(s => s.Resume(), Times.Never);
|
||||
explorerShell.Verify(s => s.Suspend(), Times.Once);
|
||||
newDesktop.Verify(d => d.Activate(), Times.Once);
|
||||
newDesktop.Verify(d => d.Close(), Times.Never);
|
||||
originalDesktop.Verify(d => d.Activate(), Times.Never);
|
||||
|
@ -145,7 +155,9 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
|
|||
settings.KioskMode = KioskMode.DisableExplorerShell;
|
||||
sut.Repeat();
|
||||
|
||||
explorerShell.Verify(s => s.Resume(), Times.Once);
|
||||
explorerShell.Verify(s => s.Terminate(), Times.Once);
|
||||
explorerShell.Verify(s => s.Suspend(), Times.Once);
|
||||
explorerShell.Verify(s => s.Start(), Times.Never);
|
||||
newDesktop.Verify(d => d.Activate(), Times.Once);
|
||||
newDesktop.Verify(d => d.Close(), Times.Once);
|
||||
|
@ -154,7 +166,9 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
|
|||
settings.KioskMode = KioskMode.CreateNewDesktop;
|
||||
sut.Repeat();
|
||||
|
||||
explorerShell.Verify(s => s.Resume(), Times.Once);
|
||||
explorerShell.Verify(s => s.Terminate(), Times.Once);
|
||||
explorerShell.Verify(s => s.Suspend(), Times.Once);
|
||||
explorerShell.Verify(s => s.Start(), Times.Once);
|
||||
newDesktop.Verify(d => d.Activate(), Times.Exactly(2));
|
||||
newDesktop.Verify(d => d.Close(), Times.Once);
|
||||
|
@ -197,6 +211,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
|
|||
desktopFactory.Verify(f => f.CreateNew(It.IsAny<string>()), Times.Once);
|
||||
newDesktop.Verify(d => d.Activate(), Times.Once);
|
||||
processFactory.VerifySet(f => f.StartupDesktop = newDesktop.Object, Times.Once);
|
||||
explorerShell.Verify(s => s.Suspend(), Times.Once);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
@ -213,11 +228,5 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
|
|||
|
||||
explorerShell.Verify(s => s.Terminate(), Times.Once);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MustRestoreOriginalDesktopInCaseOfFailure()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ namespace SafeExamBrowser.Runtime
|
|||
|
||||
internal void BuildObjectGraph(Action shutdown)
|
||||
{
|
||||
const int TEN_SECONDS = 10000;
|
||||
const int STARTUP_TIMEOUT_MS = 30000;
|
||||
|
||||
var args = Environment.GetCommandLineArgs();
|
||||
var configuration = BuildConfigurationRepository();
|
||||
|
@ -73,9 +73,9 @@ namespace SafeExamBrowser.Runtime
|
|||
sessionOperations.Enqueue(new ConfigurationOperation(appConfig, configuration, logger, messageBox, resourceLoader, runtimeHost, text, uiFactory, args));
|
||||
sessionOperations.Enqueue(new SessionInitializationOperation(configuration, logger, runtimeHost));
|
||||
sessionOperations.Enqueue(new ServiceOperation(configuration, logger, serviceProxy, text));
|
||||
sessionOperations.Enqueue(new ClientTerminationOperation(configuration, logger, processFactory, proxyFactory, runtimeHost, TEN_SECONDS));
|
||||
sessionOperations.Enqueue(new ClientTerminationOperation(configuration, logger, processFactory, proxyFactory, runtimeHost, STARTUP_TIMEOUT_MS));
|
||||
sessionOperations.Enqueue(new KioskModeOperation(configuration, desktopFactory, explorerShell, logger, processFactory));
|
||||
sessionOperations.Enqueue(new ClientOperation(configuration, logger, processFactory, proxyFactory, runtimeHost, TEN_SECONDS));
|
||||
sessionOperations.Enqueue(new ClientOperation(configuration, logger, processFactory, proxyFactory, runtimeHost, STARTUP_TIMEOUT_MS));
|
||||
|
||||
var bootstrapSequence = new OperationSequence(logger, bootstrapOperations);
|
||||
var sessionSequence = new OperationSequence(logger, sessionOperations);
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
using SafeExamBrowser.Contracts.Core.OperationModel;
|
||||
using SafeExamBrowser.Contracts.Configuration;
|
||||
using SafeExamBrowser.Contracts.Configuration.Settings;
|
||||
using SafeExamBrowser.Contracts.Core.OperationModel;
|
||||
using SafeExamBrowser.Contracts.I18n;
|
||||
using SafeExamBrowser.Contracts.Logging;
|
||||
using SafeExamBrowser.Contracts.UserInterface;
|
||||
|
@ -110,6 +110,8 @@ namespace SafeExamBrowser.Runtime.Operations
|
|||
newDesktop.Activate();
|
||||
processFactory.StartupDesktop = newDesktop;
|
||||
logger.Info("Successfully activated new desktop.");
|
||||
|
||||
explorerShell.Suspend();
|
||||
}
|
||||
|
||||
private void CloseNewDesktop()
|
||||
|
@ -134,6 +136,8 @@ namespace SafeExamBrowser.Runtime.Operations
|
|||
{
|
||||
logger.Warn($"No new desktop found when attempting to close new desktop!");
|
||||
}
|
||||
|
||||
explorerShell.Resume();
|
||||
}
|
||||
|
||||
private void TerminateExplorerShell()
|
||||
|
|
29
SafeExamBrowser.WindowsApi/Constants/ThreadAccess.cs
Normal file
29
SafeExamBrowser.WindowsApi/Constants/ThreadAccess.cs
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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.WindowsApi.Constants
|
||||
{
|
||||
/// <remarks>
|
||||
/// See https://docs.microsoft.com/en-us/windows/desktop/ProcThread/thread-security-and-access-rights.
|
||||
/// </remarks>
|
||||
[Flags]
|
||||
internal enum ThreadAccess : int
|
||||
{
|
||||
TERMINATE = 0x1,
|
||||
SUSPEND_RESUME = 0x2,
|
||||
GET_CONTEXT = 0x8,
|
||||
SET_CONTEXT = 0x10,
|
||||
SET_INFORMATION = 0x20,
|
||||
QUERY_INFORMATION = 0x40,
|
||||
SET_THREAD_TOKEN = 0x80,
|
||||
IMPERSONATE = 0x100,
|
||||
DIRECT_IMPERSONATION = 0x200
|
||||
}
|
||||
}
|
|
@ -7,6 +7,8 @@
|
|||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
@ -19,11 +21,43 @@ namespace SafeExamBrowser.WindowsApi
|
|||
{
|
||||
private ILogger logger;
|
||||
private INativeMethods nativeMethods;
|
||||
private IList<ProcessThread> suspendedThreads;
|
||||
|
||||
public ExplorerShell(ILogger logger, INativeMethods nativeMethods)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.nativeMethods = nativeMethods;
|
||||
this.suspendedThreads = new List<ProcessThread>();
|
||||
}
|
||||
|
||||
public void Resume()
|
||||
{
|
||||
const int MAX_ATTEMPTS = 3;
|
||||
|
||||
logger.Debug($"Attempting to resume all {suspendedThreads.Count} previously suspended explorer shell threads...");
|
||||
|
||||
for (var attempts = 0; suspendedThreads.Any(); attempts++)
|
||||
{
|
||||
var thread = suspendedThreads.First();
|
||||
var success = nativeMethods.ResumeThread(thread.Id);
|
||||
|
||||
if (success || attempts == MAX_ATTEMPTS)
|
||||
{
|
||||
attempts = 0;
|
||||
suspendedThreads.Remove(thread);
|
||||
|
||||
if (success)
|
||||
{
|
||||
logger.Debug($"Successfully resumed thread #{thread.Id} of explorer shell process.");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Warn($"Failed to resume thread #{thread.Id} of explorer shell process within {MAX_ATTEMPTS} attempts!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info($"Successfully resumed explorer shell process.");
|
||||
}
|
||||
|
||||
public void Start()
|
||||
|
@ -47,6 +81,39 @@ namespace SafeExamBrowser.WindowsApi
|
|||
process.Close();
|
||||
}
|
||||
|
||||
public void Suspend()
|
||||
{
|
||||
var processId = nativeMethods.GetShellProcessId();
|
||||
var explorerProcesses = System.Diagnostics.Process.GetProcessesByName("explorer");
|
||||
var shellProcess = explorerProcesses.FirstOrDefault(p => p.Id == processId);
|
||||
|
||||
if (shellProcess != null)
|
||||
{
|
||||
logger.Debug($"Found explorer shell processes with PID = {processId}.");
|
||||
|
||||
foreach (ProcessThread thread in shellProcess.Threads)
|
||||
{
|
||||
var success = nativeMethods.SuspendThread(thread.Id);
|
||||
|
||||
if (success)
|
||||
{
|
||||
suspendedThreads.Add(thread);
|
||||
logger.Debug($"Successfully suspended thread #{thread.Id} of explorer shell process.");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Warn($"Failed to suspend thread #{thread.Id} of explorer shell process!");
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info($"Successfully suspended explorer shell process with PID = {processId}.");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Info("The explorer shell can't be suspended, as it seems to not be running.");
|
||||
}
|
||||
}
|
||||
|
||||
public void Terminate()
|
||||
{
|
||||
var processId = nativeMethods.GetShellProcessId();
|
||||
|
@ -69,7 +136,7 @@ namespace SafeExamBrowser.WindowsApi
|
|||
}
|
||||
else
|
||||
{
|
||||
logger.Info("The explorer shell seems to already be terminated. Skipping this step...");
|
||||
logger.Info("The explorer shell seems to already be terminated.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using SafeExamBrowser.WindowsApi.Constants;
|
||||
using SafeExamBrowser.WindowsApi.Types;
|
||||
|
||||
namespace SafeExamBrowser.WindowsApi
|
||||
|
@ -17,6 +18,10 @@ namespace SafeExamBrowser.WindowsApi
|
|||
/// </summary>
|
||||
internal class Kernel32
|
||||
{
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static extern bool CloseHandle(IntPtr hObject);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
internal static extern bool CreateProcess(
|
||||
string lpApplicationName,
|
||||
|
@ -36,7 +41,16 @@ namespace SafeExamBrowser.WindowsApi
|
|||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
internal static extern IntPtr GetModuleHandle(string lpModuleName);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
internal static extern IntPtr OpenThread(ThreadAccess dwDesiredAccess, bool bInheritHandle, uint dwThreadId);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
internal static extern int ResumeThread(IntPtr hThread);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
internal static extern EXECUTION_STATE SetThreadExecutionState(EXECUTION_STATE esFlags);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
internal static extern int SuspendThread(IntPtr hThread);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -291,6 +291,29 @@ namespace SafeExamBrowser.WindowsApi
|
|||
User32.ShowWindow(window, (int)ShowWindowCommand.Restore);
|
||||
}
|
||||
|
||||
public bool ResumeThread(int threadId)
|
||||
{
|
||||
const int FAILURE = -1;
|
||||
var handle = Kernel32.OpenThread(ThreadAccess.SUSPEND_RESUME, false, (uint) threadId);
|
||||
|
||||
if (handle == IntPtr.Zero)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = Kernel32.ResumeThread(handle);
|
||||
var success = result != FAILURE;
|
||||
|
||||
return success;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Kernel32.CloseHandle(handle);
|
||||
}
|
||||
}
|
||||
|
||||
public void SendCloseMessageTo(IntPtr window)
|
||||
{
|
||||
User32.SendMessage(window, Constant.WM_SYSCOMMAND, (IntPtr) SystemCommand.CLOSE, IntPtr.Zero);
|
||||
|
@ -309,12 +332,35 @@ namespace SafeExamBrowser.WindowsApi
|
|||
public void SetWorkingArea(IBounds bounds)
|
||||
{
|
||||
var workingArea = new RECT { Left = bounds.Left, Top = bounds.Top, Right = bounds.Right, Bottom = bounds.Bottom };
|
||||
var success = User32.SystemParametersInfo(SPI.SETWORKAREA, 0, ref workingArea, SPIF.UPDATEANDCHANGE);
|
||||
var success = User32.SystemParametersInfo(SPI.SETWORKAREA, 0, ref workingArea, SPIF.NONE);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
}
|
||||
}
|
||||
|
||||
public bool SuspendThread(int threadId)
|
||||
{
|
||||
const int FAILURE = -1;
|
||||
var handle = Kernel32.OpenThread(ThreadAccess.SUSPEND_RESUME, false, (uint) threadId);
|
||||
|
||||
if (handle == IntPtr.Zero)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = Kernel32.SuspendThread(handle);
|
||||
var success = result != FAILURE;
|
||||
|
||||
return success;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Kernel32.CloseHandle(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@
|
|||
<ItemGroup>
|
||||
<Compile Include="Constants\Constant.cs" />
|
||||
<Compile Include="Constants\HookType.cs" />
|
||||
<Compile Include="Constants\ThreadAccess.cs" />
|
||||
<Compile Include="Delegates\EnumDesktopDelegate.cs" />
|
||||
<Compile Include="Delegates\EnumWindowsDelegate.cs" />
|
||||
<Compile Include="Delegates\EventDelegate.cs" />
|
||||
|
|
Loading…
Reference in a new issue