diff --git a/SafeExamBrowser.Configuration/ConfigurationRepository.cs b/SafeExamBrowser.Configuration/ConfigurationRepository.cs
index af18ef18..280068db 100644
--- a/SafeExamBrowser.Configuration/ConfigurationRepository.cs
+++ b/SafeExamBrowser.Configuration/ConfigurationRepository.cs
@@ -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";
diff --git a/SafeExamBrowser.Contracts/WindowsApi/IExplorerShell.cs b/SafeExamBrowser.Contracts/WindowsApi/IExplorerShell.cs
index 768a1e5b..e46d7b6a 100644
--- a/SafeExamBrowser.Contracts/WindowsApi/IExplorerShell.cs
+++ b/SafeExamBrowser.Contracts/WindowsApi/IExplorerShell.cs
@@ -13,11 +13,21 @@ namespace SafeExamBrowser.Contracts.WindowsApi
///
public interface IExplorerShell
{
+ ///
+ /// Resumes the explorer shell process, if it was previously suspended.
+ ///
+ void Resume();
+
///
/// Starts the Windows explorer shell, if it isn't already running.
///
void Start();
+ ///
+ /// Suspends the explorer shell process, if it is running.
+ ///
+ void Suspend();
+
///
/// Gracefully terminates the Windows explorer shell, if it is running.
///
diff --git a/SafeExamBrowser.Contracts/WindowsApi/INativeMethods.cs b/SafeExamBrowser.Contracts/WindowsApi/INativeMethods.cs
index 9e37ca7c..e1351a5a 100644
--- a/SafeExamBrowser.Contracts/WindowsApi/INativeMethods.cs
+++ b/SafeExamBrowser.Contracts/WindowsApi/INativeMethods.cs
@@ -153,6 +153,12 @@ namespace SafeExamBrowser.Contracts.WindowsApi
///
void RestoreWindow(IntPtr window);
+ ///
+ /// Attempts to resume the thread referenced by the given thread ID. Returns true if the thread was successfully resumed,
+ /// otherwise false.
+ ///
+ bool ResumeThread(int threadId);
+
///
/// Sends a close message to the given window.
///
@@ -173,5 +179,11 @@ namespace SafeExamBrowser.Contracts.WindowsApi
/// If the working area could not be set.
///
void SetWorkingArea(IBounds bounds);
+
+ ///
+ /// Attempts to suspend the thread referenced by the given thread ID. Returns true if the thread was successfully suspended,
+ /// otherwise false.
+ ///
+ bool SuspendThread(int threadId);
}
}
diff --git a/SafeExamBrowser.Runtime.UnitTests/Operations/KioskModeOperationTests.cs b/SafeExamBrowser.Runtime.UnitTests/Operations/KioskModeOperationTests.cs
index 19834db0..1b85d9d5 100644
--- a/SafeExamBrowser.Runtime.UnitTests/Operations/KioskModeOperationTests.cs
+++ b/SafeExamBrowser.Runtime.UnitTests/Operations/KioskModeOperationTests.cs
@@ -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())).Callback(() => createNew = ++order).Returns(newDesktop.Object);
newDesktop.Setup(d => d.Activate()).Callback(() => activate = ++order);
processFactory.SetupSet(f => f.StartupDesktop = It.IsAny()).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()), 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(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()), 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()
- {
-
- }
}
}
diff --git a/SafeExamBrowser.Runtime/CompositionRoot.cs b/SafeExamBrowser.Runtime/CompositionRoot.cs
index d69cd911..04b05fcf 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 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);
diff --git a/SafeExamBrowser.Runtime/Operations/KioskModeOperation.cs b/SafeExamBrowser.Runtime/Operations/KioskModeOperation.cs
index 9f1e0caa..a75a36bf 100644
--- a/SafeExamBrowser.Runtime/Operations/KioskModeOperation.cs
+++ b/SafeExamBrowser.Runtime/Operations/KioskModeOperation.cs
@@ -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()
diff --git a/SafeExamBrowser.WindowsApi/Constants/ThreadAccess.cs b/SafeExamBrowser.WindowsApi/Constants/ThreadAccess.cs
new file mode 100644
index 00000000..958b1e16
--- /dev/null
+++ b/SafeExamBrowser.WindowsApi/Constants/ThreadAccess.cs
@@ -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
+{
+ ///
+ /// See https://docs.microsoft.com/en-us/windows/desktop/ProcThread/thread-security-and-access-rights.
+ ///
+ [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
+ }
+}
diff --git a/SafeExamBrowser.WindowsApi/ExplorerShell.cs b/SafeExamBrowser.WindowsApi/ExplorerShell.cs
index b671619b..2ba83ca9 100644
--- a/SafeExamBrowser.WindowsApi/ExplorerShell.cs
+++ b/SafeExamBrowser.WindowsApi/ExplorerShell.cs
@@ -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 suspendedThreads;
public ExplorerShell(ILogger logger, INativeMethods nativeMethods)
{
this.logger = logger;
this.nativeMethods = nativeMethods;
+ this.suspendedThreads = new List();
+ }
+
+ 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.");
}
}
}
diff --git a/SafeExamBrowser.WindowsApi/Kernel32.cs b/SafeExamBrowser.WindowsApi/Kernel32.cs
index dacffd5e..f66ddbcf 100644
--- a/SafeExamBrowser.WindowsApi/Kernel32.cs
+++ b/SafeExamBrowser.WindowsApi/Kernel32.cs
@@ -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
///
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);
}
}
diff --git a/SafeExamBrowser.WindowsApi/NativeMethods.cs b/SafeExamBrowser.WindowsApi/NativeMethods.cs
index ded4adda..c8197c2c 100644
--- a/SafeExamBrowser.WindowsApi/NativeMethods.cs
+++ b/SafeExamBrowser.WindowsApi/NativeMethods.cs
@@ -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);
+ }
+ }
}
}
diff --git a/SafeExamBrowser.WindowsApi/SafeExamBrowser.WindowsApi.csproj b/SafeExamBrowser.WindowsApi/SafeExamBrowser.WindowsApi.csproj
index 15a43937..8ff0a0e4 100644
--- a/SafeExamBrowser.WindowsApi/SafeExamBrowser.WindowsApi.csproj
+++ b/SafeExamBrowser.WindowsApi/SafeExamBrowser.WindowsApi.csproj
@@ -53,6 +53,7 @@
+