From 5d05acb6d730ca8c6e27b1bde4622e088e5c1b71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damian=20B=C3=BCchel?= Date: Fri, 16 Apr 2021 19:12:56 +0200 Subject: [PATCH] SEBWIN-453: Implemented monitoring for kiosk mode Create New Desktop. --- .../Operations/KioskModeOperationTests.cs | 18 +++- SafeExamBrowser.Runtime/CompositionRoot.cs | 3 +- .../Operations/KioskModeOperation.cs | 7 ++ .../IDesktopMonitor.cs | 26 ++++++ ...afeExamBrowser.WindowsApi.Contracts.csproj | 1 + SafeExamBrowser.WindowsApi/DesktopMonitor.cs | 89 +++++++++++++++++++ .../SafeExamBrowser.WindowsApi.csproj | 1 + SafeExamBrowser.WindowsApi/User32.cs | 3 + 8 files changed, 143 insertions(+), 5 deletions(-) create mode 100644 SafeExamBrowser.WindowsApi.Contracts/IDesktopMonitor.cs create mode 100644 SafeExamBrowser.WindowsApi/DesktopMonitor.cs diff --git a/SafeExamBrowser.Runtime.UnitTests/Operations/KioskModeOperationTests.cs b/SafeExamBrowser.Runtime.UnitTests/Operations/KioskModeOperationTests.cs index a58b93a1..9b85643b 100644 --- a/SafeExamBrowser.Runtime.UnitTests/Operations/KioskModeOperationTests.cs +++ b/SafeExamBrowser.Runtime.UnitTests/Operations/KioskModeOperationTests.cs @@ -24,6 +24,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations private SessionConfiguration currentSession; private AppSettings currentSettings; private Mock desktopFactory; + private Mock desktopMonitor; private Mock explorerShell; private Mock logger; private SessionConfiguration nextSession; @@ -39,6 +40,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations currentSession = new SessionConfiguration(); currentSettings = new AppSettings(); desktopFactory = new Mock(); + desktopMonitor = new Mock(); explorerShell = new Mock(); logger = new Mock(); nextSession = new SessionConfiguration(); @@ -51,7 +53,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations sessionContext.Current = currentSession; sessionContext.Next = nextSession; - sut = new KioskModeOperation(desktopFactory.Object, explorerShell.Object, logger.Object, processFactory.Object, sessionContext); + sut = new KioskModeOperation(desktopFactory.Object, desktopMonitor.Object, explorerShell.Object, logger.Object, processFactory.Object, sessionContext); } [TestMethod] @@ -64,6 +66,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations var createNew = 0; var activate = 0; var setStartup = 0; + var startMonitor = 0; nextSettings.Security.KioskMode = KioskMode.CreateNewDesktop; @@ -71,6 +74,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); + desktopMonitor.Setup(m => m.Start(It.IsAny())).Callback(() => startMonitor = ++order); var result = sut.Perform(); @@ -86,6 +90,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations Assert.AreEqual(2, createNew); Assert.AreEqual(3, activate); Assert.AreEqual(4, setStartup); + Assert.AreEqual(5, startMonitor); } [TestMethod] @@ -366,6 +371,8 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations desktopFactory.Verify(f => f.GetCurrent(), Times.Once); desktopFactory.Verify(f => f.CreateNew(It.IsAny()), Times.Once); + desktopMonitor.Verify(m => m.Start(It.IsAny()), Times.Once); + desktopMonitor.Verify(m => m.Stop(), Times.Never); explorerShell.VerifyNoOtherCalls(); newDesktop.Verify(d => d.Activate(), Times.Once); newDesktop.Verify(d => d.Close(), Times.Never); @@ -403,6 +410,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations var order = 0; var activate = 0; var setStartup = 0; + var stopMonitor = 0; var close = 0; currentSettings.Security.KioskMode = KioskMode.CreateNewDesktop; @@ -415,6 +423,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations Assert.AreEqual(OperationResult.Success, performResult); desktopFactory.Reset(); + desktopMonitor.Setup(m => m.Stop()).Callback(() => stopMonitor = ++order); explorerShell.Reset(); originalDesktop.Reset(); originalDesktop.Setup(d => d.Activate()).Callback(() => activate = ++order); @@ -432,9 +441,10 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations Assert.AreEqual(OperationResult.Success, performResult); Assert.AreEqual(OperationResult.Success, revertResult); - Assert.AreEqual(1, activate); - Assert.AreEqual(2, setStartup); - Assert.AreEqual(3, close); + Assert.AreEqual(1, stopMonitor); + Assert.AreEqual(2, activate); + Assert.AreEqual(3, setStartup); + Assert.AreEqual(4, close); } [TestMethod] diff --git a/SafeExamBrowser.Runtime/CompositionRoot.cs b/SafeExamBrowser.Runtime/CompositionRoot.cs index 4174c9c3..f12daa81 100644 --- a/SafeExamBrowser.Runtime/CompositionRoot.cs +++ b/SafeExamBrowser.Runtime/CompositionRoot.cs @@ -62,6 +62,7 @@ namespace SafeExamBrowser.Runtime var nativeMethods = new NativeMethods(); var uiFactory = new UserInterfaceFactory(text); var desktopFactory = new DesktopFactory(ModuleLogger(nameof(DesktopFactory))); + var desktopMonitor = new DesktopMonitor(ModuleLogger(nameof(DesktopMonitor))); var explorerShell = new ExplorerShell(ModuleLogger(nameof(ExplorerShell)), nativeMethods); var fileSystem = new FileSystem(); var processFactory = new ProcessFactory(ModuleLogger(nameof(ProcessFactory))); @@ -91,7 +92,7 @@ namespace SafeExamBrowser.Runtime sessionOperations.Enqueue(new ServiceOperation(logger, runtimeHost, serviceProxy, sessionContext, THIRTY_SECONDS, userInfo)); sessionOperations.Enqueue(new ClientTerminationOperation(logger, processFactory, proxyFactory, runtimeHost, sessionContext, THIRTY_SECONDS)); sessionOperations.Enqueue(new ProctoringWorkaroundOperation(logger, sessionContext)); - sessionOperations.Enqueue(new KioskModeOperation(desktopFactory, explorerShell, logger, processFactory, sessionContext)); + sessionOperations.Enqueue(new KioskModeOperation(desktopFactory, desktopMonitor, explorerShell, logger, processFactory, sessionContext)); sessionOperations.Enqueue(new ClientOperation(logger, processFactory, proxyFactory, runtimeHost, sessionContext, THIRTY_SECONDS)); sessionOperations.Enqueue(new SessionActivationOperation(logger, sessionContext)); diff --git a/SafeExamBrowser.Runtime/Operations/KioskModeOperation.cs b/SafeExamBrowser.Runtime/Operations/KioskModeOperation.cs index d5ddcbb5..a8814631 100644 --- a/SafeExamBrowser.Runtime/Operations/KioskModeOperation.cs +++ b/SafeExamBrowser.Runtime/Operations/KioskModeOperation.cs @@ -20,6 +20,7 @@ namespace SafeExamBrowser.Runtime.Operations private IDesktop newDesktop; private IDesktop originalDesktop; private IDesktopFactory desktopFactory; + private IDesktopMonitor desktopMonitor; private IExplorerShell explorerShell; private KioskMode? activeMode; private ILogger logger; @@ -30,12 +31,14 @@ namespace SafeExamBrowser.Runtime.Operations public KioskModeOperation( IDesktopFactory desktopFactory, + IDesktopMonitor desktopMonitor, IExplorerShell explorerShell, ILogger logger, IProcessFactory processFactory, SessionContext sessionContext) : base(sessionContext) { this.desktopFactory = desktopFactory; + this.desktopMonitor = desktopMonitor; this.explorerShell = explorerShell; this.logger = logger; this.processFactory = processFactory; @@ -129,10 +132,14 @@ namespace SafeExamBrowser.Runtime.Operations newDesktop.Activate(); processFactory.StartupDesktop = newDesktop; logger.Info("Successfully activated new desktop."); + + desktopMonitor.Start(newDesktop); } private void CloseNewDesktop() { + desktopMonitor.Stop(); + if (originalDesktop != null) { originalDesktop.Activate(); diff --git a/SafeExamBrowser.WindowsApi.Contracts/IDesktopMonitor.cs b/SafeExamBrowser.WindowsApi.Contracts/IDesktopMonitor.cs new file mode 100644 index 00000000..4b689278 --- /dev/null +++ b/SafeExamBrowser.WindowsApi.Contracts/IDesktopMonitor.cs @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2021 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.WindowsApi.Contracts +{ + /// + /// Provides funcionality to monitor a desktop, i.e. ensure a given desktop remains active even when a desktop switch is performed. + /// + public interface IDesktopMonitor + { + /// + /// Starts to monitor the given desktop. + /// + void Start(IDesktop desktop); + + /// + /// Stops the monitoring. + /// + void Stop(); + } +} diff --git a/SafeExamBrowser.WindowsApi.Contracts/SafeExamBrowser.WindowsApi.Contracts.csproj b/SafeExamBrowser.WindowsApi.Contracts/SafeExamBrowser.WindowsApi.Contracts.csproj index 7e240162..e8b7123f 100644 --- a/SafeExamBrowser.WindowsApi.Contracts/SafeExamBrowser.WindowsApi.Contracts.csproj +++ b/SafeExamBrowser.WindowsApi.Contracts/SafeExamBrowser.WindowsApi.Contracts.csproj @@ -67,6 +67,7 @@ + diff --git a/SafeExamBrowser.WindowsApi/DesktopMonitor.cs b/SafeExamBrowser.WindowsApi/DesktopMonitor.cs new file mode 100644 index 00000000..f81bc0c4 --- /dev/null +++ b/SafeExamBrowser.WindowsApi/DesktopMonitor.cs @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2021 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.Timers; +using SafeExamBrowser.Logging.Contracts; +using SafeExamBrowser.WindowsApi.Constants; +using SafeExamBrowser.WindowsApi.Contracts; + +namespace SafeExamBrowser.WindowsApi +{ + public class DesktopMonitor : IDesktopMonitor + { + private readonly ILogger logger; + private readonly Timer timer; + + private IDesktop desktop; + + public DesktopMonitor(ILogger logger) + { + this.logger = logger; + this.timer = new Timer(1000); + } + + public void Start(IDesktop desktop) + { + this.desktop = desktop; + + timer.AutoReset = false; + timer.Elapsed += Timer_Elapsed; + timer.Start(); + + logger.Info($"Started monitoring desktop {desktop}."); + } + + public void Stop() + { + timer.Stop(); + timer.Elapsed -= Timer_Elapsed; + + logger.Info($"Stopped monitoring desktop {desktop}."); + } + + private void Timer_Elapsed(object sender, ElapsedEventArgs e) + { + var handle = User32.OpenInputDesktop(0, false, (uint) AccessMask.DESKTOP_NONE); + var name = string.Empty; + var nameLength = 0; + + if (handle != IntPtr.Zero) + { + User32.GetUserObjectInformation(handle, Constant.UOI_NAME, IntPtr.Zero, 0, ref nameLength); + + var namePointer = Marshal.AllocHGlobal(nameLength); + var success = User32.GetUserObjectInformation(handle, Constant.UOI_NAME, namePointer, nameLength, ref nameLength); + + if (success) + { + name = Marshal.PtrToStringAnsi(namePointer); + Marshal.FreeHGlobal(namePointer); + + if (name?.Equals(desktop.Name, StringComparison.OrdinalIgnoreCase) != true) + { + logger.Warn($"Detected desktop switch to '{name}' [{handle}], trying to reactivate {desktop}..."); + desktop.Activate(); + } + } + else + { + logger.Warn("Failed to get name of currently active desktop!"); + } + + User32.CloseDesktop(handle); + } + else + { + logger.Warn("Failed to get currently active desktop!"); + } + + timer.Start(); + } + } +} diff --git a/SafeExamBrowser.WindowsApi/SafeExamBrowser.WindowsApi.csproj b/SafeExamBrowser.WindowsApi/SafeExamBrowser.WindowsApi.csproj index 7494b737..63861e8d 100644 --- a/SafeExamBrowser.WindowsApi/SafeExamBrowser.WindowsApi.csproj +++ b/SafeExamBrowser.WindowsApi/SafeExamBrowser.WindowsApi.csproj @@ -65,6 +65,7 @@ + diff --git a/SafeExamBrowser.WindowsApi/User32.cs b/SafeExamBrowser.WindowsApi/User32.cs index 4aaa2bb3..0dbe400b 100644 --- a/SafeExamBrowser.WindowsApi/User32.cs +++ b/SafeExamBrowser.WindowsApi/User32.cs @@ -77,6 +77,9 @@ namespace SafeExamBrowser.WindowsApi [DllImport("user32.dll", SetLastError = true)] internal static extern bool OpenClipboard(IntPtr hWndNewOwner); + [DllImport("user32.dll", SetLastError = true)] + internal static extern IntPtr OpenInputDesktop(uint dwFlags, bool fInherit, uint dwDesiredAccess); + [DllImport("user32.dll", SetLastError = true)] internal static extern bool PostMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);