diff --git a/SafeExamBrowser.Client.UnitTests/ClientControllerTests.cs b/SafeExamBrowser.Client.UnitTests/ClientControllerTests.cs index dcec8d5f..da96385b 100644 --- a/SafeExamBrowser.Client.UnitTests/ClientControllerTests.cs +++ b/SafeExamBrowser.Client.UnitTests/ClientControllerTests.cs @@ -14,6 +14,7 @@ using Moq; using SafeExamBrowser.Applications.Contracts; using SafeExamBrowser.Browser.Contracts; using SafeExamBrowser.Browser.Contracts.Events; +using SafeExamBrowser.Client.Contracts; using SafeExamBrowser.Client.Operations.Events; using SafeExamBrowser.Communication.Contracts.Data; using SafeExamBrowser.Communication.Contracts.Events; @@ -56,6 +57,7 @@ namespace SafeExamBrowser.Client.UnitTests private Mock browser; private Mock clientHost; private ClientContext context; + private Mock coordinator; private Mock displayMonitor; private Mock explorerShell; private Mock fileSystemDialog; @@ -90,6 +92,7 @@ namespace SafeExamBrowser.Client.UnitTests browser = new Mock(); clientHost = new Mock(); context = new ClientContext(); + coordinator = new Mock(); displayMonitor = new Mock(); explorerShell = new Mock(); fileSystemDialog = new Mock(); @@ -120,6 +123,7 @@ namespace SafeExamBrowser.Client.UnitTests actionCenter.Object, applicationMonitor.Object, context, + coordinator.Object, displayMonitor.Object, explorerShell.Object, fileSystemDialog.Object, @@ -729,13 +733,17 @@ namespace SafeExamBrowser.Client.UnitTests var args = new DownloadEventArgs(); appConfig.TemporaryDirectory = @"C:\Folder\Does\Not\Exist"; + coordinator.Setup(c => c.RequestReconfigurationLock()).Returns(true); runtimeProxy.Setup(r => r.RequestReconfiguration(It.IsAny(), It.IsAny())).Returns(new CommunicationResult(true)); sut.TryStart(); browser.Raise(b => b.ConfigurationDownloadRequested += null, "filepath.seb", args); args.Callback(true, string.Empty); + coordinator.Verify(c => c.RequestReconfigurationLock(), Times.Once); + coordinator.Verify(c => c.ReleaseReconfigurationLock(), Times.Never); runtimeProxy.Verify(r => r.RequestReconfiguration(It.IsAny(), It.IsAny()), Times.Once); + Assert.IsTrue(args.AllowDownload); } @@ -752,7 +760,30 @@ namespace SafeExamBrowser.Client.UnitTests sut.TryStart(); browser.Raise(b => b.ConfigurationDownloadRequested += null, "filepath.seb", args); + coordinator.Verify(c => c.RequestReconfigurationLock(), Times.Never); + coordinator.Verify(c => c.ReleaseReconfigurationLock(), Times.Never); runtimeProxy.Verify(r => r.RequestReconfiguration(It.IsAny(), It.IsAny()), Times.Never); + + Assert.IsFalse(args.AllowDownload); + } + + [TestMethod] + public void Reconfiguration_MustNotAllowConcurrentExecution() + { + var args = new DownloadEventArgs(); + + appConfig.TemporaryDirectory = @"C:\Folder\Does\Not\Exist"; + coordinator.Setup(c => c.RequestReconfigurationLock()).Returns(false); + runtimeProxy.Setup(r => r.RequestReconfiguration(It.IsAny(), It.IsAny())).Returns(new CommunicationResult(true)); + + sut.TryStart(); + browser.Raise(b => b.ConfigurationDownloadRequested += null, "filepath.seb", args); + args.Callback?.Invoke(true, string.Empty); + + coordinator.Verify(c => c.RequestReconfigurationLock(), Times.Once); + coordinator.Verify(c => c.ReleaseReconfigurationLock(), Times.Never); + runtimeProxy.Verify(r => r.RequestReconfiguration(It.IsAny(), It.IsAny()), Times.Never); + Assert.IsFalse(args.AllowDownload); } @@ -762,6 +793,7 @@ namespace SafeExamBrowser.Client.UnitTests var args = new DownloadEventArgs { Url = "sebs://www.somehost.org/some/path/some_configuration.seb?query=123" }; appConfig.TemporaryDirectory = @"C:\Folder\Does\Not\Exist"; + coordinator.Setup(c => c.RequestReconfigurationLock()).Returns(true); settings.Security.AllowReconfiguration = true; settings.Security.QuitPasswordHash = "abc123"; settings.Security.ReconfigurationUrl = "sebs://www.somehost.org/some/path/*.seb?query=123"; @@ -771,7 +803,10 @@ namespace SafeExamBrowser.Client.UnitTests browser.Raise(b => b.ConfigurationDownloadRequested += null, "filepath.seb", args); args.Callback(true, string.Empty); + coordinator.Verify(c => c.RequestReconfigurationLock(), Times.Once); + coordinator.Verify(c => c.ReleaseReconfigurationLock(), Times.Never); runtimeProxy.Verify(r => r.RequestReconfiguration(It.IsAny(), It.IsAny()), Times.Once); + Assert.IsTrue(args.AllowDownload); } @@ -786,7 +821,10 @@ namespace SafeExamBrowser.Client.UnitTests sut.TryStart(); browser.Raise(b => b.ConfigurationDownloadRequested += null, "filepath.seb", args); + coordinator.Verify(c => c.RequestReconfigurationLock(), Times.Never); + coordinator.Verify(c => c.ReleaseReconfigurationLock(), Times.Never); runtimeProxy.Verify(r => r.RequestReconfiguration(It.IsAny(), It.IsAny()), Times.Never); + Assert.IsFalse(args.AllowDownload); } @@ -802,7 +840,10 @@ namespace SafeExamBrowser.Client.UnitTests sut.TryStart(); browser.Raise(b => b.ConfigurationDownloadRequested += null, "filepath.seb", args); + coordinator.Verify(c => c.RequestReconfigurationLock(), Times.Never); + coordinator.Verify(c => c.ReleaseReconfigurationLock(), Times.Never); runtimeProxy.Verify(r => r.RequestReconfiguration(It.IsAny(), It.IsAny()), Times.Never); + Assert.IsFalse(args.AllowDownload); } @@ -815,7 +856,7 @@ namespace SafeExamBrowser.Client.UnitTests var args = new DownloadEventArgs(); appConfig.TemporaryDirectory = @"C:\Folder\Does\Not\Exist"; - settings.Security.AllowReconfiguration = true; + coordinator.Setup(c => c.RequestReconfigurationLock()).Returns(true); messageBox.Setup(m => m.Show( It.IsAny(), It.IsAny(), @@ -825,11 +866,14 @@ namespace SafeExamBrowser.Client.UnitTests runtimeProxy.Setup(r => r.RequestReconfiguration( It.Is(p => p == downloadPath), It.Is(u => u == downloadUrl))).Returns(new CommunicationResult(true)); + settings.Security.AllowReconfiguration = true; sut.TryStart(); browser.Raise(b => b.ConfigurationDownloadRequested += null, filename, args); args.Callback(true, downloadUrl, downloadPath); + coordinator.Verify(c => c.RequestReconfigurationLock(), Times.Once); + coordinator.Verify(c => c.ReleaseReconfigurationLock(), Times.Never); runtimeProxy.Verify(r => r.RequestReconfiguration(It.Is(p => p == downloadPath), It.Is(u => u == downloadUrl)), Times.Once); Assert.AreEqual(downloadPath, args.DownloadPath); @@ -845,7 +889,7 @@ namespace SafeExamBrowser.Client.UnitTests var args = new DownloadEventArgs(); appConfig.TemporaryDirectory = @"C:\Folder\Does\Not\Exist"; - settings.Security.AllowReconfiguration = true; + coordinator.Setup(c => c.RequestReconfigurationLock()).Returns(true); messageBox.Setup(m => m.Show( It.IsAny(), It.IsAny(), @@ -855,11 +899,14 @@ namespace SafeExamBrowser.Client.UnitTests runtimeProxy.Setup(r => r.RequestReconfiguration( It.Is(p => p == downloadPath), It.Is(u => u == downloadUrl))).Returns(new CommunicationResult(true)); + settings.Security.AllowReconfiguration = true; sut.TryStart(); browser.Raise(b => b.ConfigurationDownloadRequested += null, filename, args); args.Callback(false, downloadPath); + coordinator.Verify(c => c.RequestReconfigurationLock(), Times.Once); + coordinator.Verify(c => c.ReleaseReconfigurationLock(), Times.Once); runtimeProxy.Verify(r => r.RequestReconfiguration(It.IsAny(), It.IsAny()), Times.Never); } @@ -872,7 +919,7 @@ namespace SafeExamBrowser.Client.UnitTests var args = new DownloadEventArgs(); appConfig.TemporaryDirectory = @"C:\Folder\Does\Not\Exist"; - settings.Security.AllowReconfiguration = true; + coordinator.Setup(c => c.RequestReconfigurationLock()).Returns(true); messageBox.Setup(m => m.Show( It.IsAny(), It.IsAny(), @@ -882,18 +929,21 @@ namespace SafeExamBrowser.Client.UnitTests runtimeProxy.Setup(r => r.RequestReconfiguration( It.Is(p => p == downloadPath), It.Is(u => u == downloadUrl))).Returns(new CommunicationResult(false)); + settings.Security.AllowReconfiguration = true; sut.TryStart(); browser.Raise(b => b.ConfigurationDownloadRequested += null, filename, args); args.Callback(true, downloadUrl, downloadPath); - runtimeProxy.Verify(r => r.RequestReconfiguration(It.IsAny(), It.IsAny()), Times.Once); + coordinator.Verify(c => c.RequestReconfigurationLock(), Times.Once); + coordinator.Verify(c => c.ReleaseReconfigurationLock(), Times.Once); messageBox.Verify(m => m.Show( It.IsAny(), It.IsAny(), It.IsAny(), It.Is(i => i == MessageBoxIcon.Error), It.IsAny()), Times.Once); + runtimeProxy.Verify(r => r.RequestReconfiguration(It.IsAny(), It.IsAny()), Times.Once); } [TestMethod] @@ -1231,8 +1281,9 @@ namespace SafeExamBrowser.Client.UnitTests { var lockScreen = new Mock(); - settings.Service.IgnoreService = true; + coordinator.Setup(c => c.RequestSessionLock()).Returns(true); lockScreen.Setup(l => l.WaitForResult()).Returns(new LockScreenResult()); + settings.Service.IgnoreService = true; uiFactory .Setup(f => f.CreateLockScreen(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())) .Returns(lockScreen.Object); @@ -1240,6 +1291,8 @@ namespace SafeExamBrowser.Client.UnitTests sut.TryStart(); systemMonitor.Raise(m => m.SessionChanged += null); + coordinator.Verify(c => c.RequestSessionLock(), Times.Once); + coordinator.Verify(c => c.ReleaseSessionLock(), Times.Once); lockScreen.Verify(l => l.Show(), Times.Once); } @@ -1249,9 +1302,10 @@ namespace SafeExamBrowser.Client.UnitTests var lockScreen = new Mock(); var result = new LockScreenResult(); - settings.Service.IgnoreService = true; + coordinator.Setup(c => c.RequestSessionLock()).Returns(true); lockScreen.Setup(l => l.WaitForResult()).Returns(result); runtimeProxy.Setup(r => r.RequestShutdown()).Returns(new CommunicationResult(true)); + settings.Service.IgnoreService = true; uiFactory .Setup(f => f.CreateLockScreen(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())) .Callback(new Action, LockScreenSettings>((message, title, options, settings) => result.OptionId = options.Last().Id)) @@ -1260,6 +1314,8 @@ namespace SafeExamBrowser.Client.UnitTests sut.TryStart(); systemMonitor.Raise(m => m.SessionChanged += null); + coordinator.Verify(c => c.RequestSessionLock(), Times.Once); + coordinator.Verify(c => c.ReleaseSessionLock(), Times.Once); lockScreen.Verify(l => l.Show(), Times.Once); runtimeProxy.Verify(p => p.RequestShutdown(), Times.Once); } diff --git a/SafeExamBrowser.Client.UnitTests/CoordinatorTests.cs b/SafeExamBrowser.Client.UnitTests/CoordinatorTests.cs new file mode 100644 index 00000000..d27abcbc --- /dev/null +++ b/SafeExamBrowser.Client.UnitTests/CoordinatorTests.cs @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2024 ETH Zürich, IT Services + * + * 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.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace SafeExamBrowser.Client.UnitTests +{ + [TestClass] + public class CoordinatorTests + { + private Coordinator sut; + + [TestInitialize] + public void Initialize() + { + sut = new Coordinator(); + } + + [TestMethod] + public void ReconfigurationLock_MustWorkCorrectly() + { + Assert.IsFalse(sut.IsReconfigurationLocked()); + + sut.RequestReconfigurationLock(); + + var result = Parallel.For(1, 1000, (_) => + { + Assert.IsTrue(sut.IsReconfigurationLocked()); + Assert.IsFalse(sut.RequestReconfigurationLock()); + }); + + Assert.IsTrue(result.IsCompleted); + + result = Parallel.For(1, 1000, (_) => + { + sut.ReleaseReconfigurationLock(); + }); + + Assert.IsFalse(sut.IsReconfigurationLocked()); + Assert.IsTrue(result.IsCompleted); + } + + [TestMethod] + public void RequestReconfigurationLock_MustOnlyAllowLockingOnce() + { + var count = 0; + + Assert.IsFalse(sut.IsReconfigurationLocked()); + + var result = Parallel.For(1, 1000, (_) => + { + var acquired = sut.RequestReconfigurationLock(); + + if (acquired) + { + Interlocked.Increment(ref count); + } + }); + + Assert.AreEqual(1, count); + Assert.IsTrue(sut.IsReconfigurationLocked()); + Assert.IsTrue(result.IsCompleted); + } + + [TestMethod] + public void RequestSessionLock_MustOnlyAllowLockingOnce() + { + var count = 0; + + Assert.IsFalse(sut.IsSessionLocked()); + + var result = Parallel.For(1, 1000, (_) => + { + var acquired = sut.RequestSessionLock(); + + if (acquired) + { + Interlocked.Increment(ref count); + } + }); + + Assert.AreEqual(1, count); + Assert.IsTrue(sut.IsSessionLocked()); + Assert.IsTrue(result.IsCompleted); + } + + [TestMethod] + public void SessionLock_MustWorkCorrectly() + { + Assert.IsFalse(sut.IsSessionLocked()); + + sut.RequestSessionLock(); + + var result = Parallel.For(1, 1000, (_) => + { + Assert.IsTrue(sut.IsSessionLocked()); + Assert.IsFalse(sut.RequestSessionLock()); + }); + + Assert.IsTrue(result.IsCompleted); + + result = Parallel.For(1, 1000, (_) => + { + sut.ReleaseSessionLock(); + }); + + Assert.IsFalse(sut.IsSessionLocked()); + Assert.IsTrue(result.IsCompleted); + } + } +} diff --git a/SafeExamBrowser.Client.UnitTests/SafeExamBrowser.Client.UnitTests.csproj b/SafeExamBrowser.Client.UnitTests/SafeExamBrowser.Client.UnitTests.csproj index 6bf5939e..d84e9bb8 100644 --- a/SafeExamBrowser.Client.UnitTests/SafeExamBrowser.Client.UnitTests.csproj +++ b/SafeExamBrowser.Client.UnitTests/SafeExamBrowser.Client.UnitTests.csproj @@ -166,6 +166,7 @@ + diff --git a/SafeExamBrowser.Client/ClientController.cs b/SafeExamBrowser.Client/ClientController.cs index ebe48976..00cf42f9 100644 --- a/SafeExamBrowser.Client/ClientController.cs +++ b/SafeExamBrowser.Client/ClientController.cs @@ -16,6 +16,7 @@ using System.Threading.Tasks; using SafeExamBrowser.Applications.Contracts; using SafeExamBrowser.Browser.Contracts; using SafeExamBrowser.Browser.Contracts.Events; +using SafeExamBrowser.Client.Contracts; using SafeExamBrowser.Client.Operations.Events; using SafeExamBrowser.Communication.Contracts.Data; using SafeExamBrowser.Communication.Contracts.Events; @@ -53,6 +54,7 @@ namespace SafeExamBrowser.Client private readonly IActionCenter actionCenter; private readonly IApplicationMonitor applicationMonitor; private readonly ClientContext context; + private readonly ICoordinator coordinator; private readonly IDisplayMonitor displayMonitor; private readonly IExplorerShell explorerShell; private readonly IFileSystemDialog fileSystemDialog; @@ -78,12 +80,12 @@ namespace SafeExamBrowser.Client private AppSettings Settings => context.Settings; private ILockScreen lockScreen; - private bool sessionLocked; internal ClientController( IActionCenter actionCenter, IApplicationMonitor applicationMonitor, ClientContext context, + ICoordinator coordinator, IDisplayMonitor displayMonitor, IExplorerShell explorerShell, IFileSystemDialog fileSystemDialog, @@ -104,6 +106,7 @@ namespace SafeExamBrowser.Client this.actionCenter = actionCenter; this.applicationMonitor = applicationMonitor; this.context = context; + this.coordinator = coordinator; this.displayMonitor = displayMonitor; this.explorerShell = explorerShell; this.fileSystemDialog = fileSystemDialog; @@ -487,6 +490,36 @@ namespace SafeExamBrowser.Client } private void Browser_ConfigurationDownloadRequested(string fileName, DownloadEventArgs args) + { + args.AllowDownload = false; + + if (IsAllowedToReconfigure(args.Url)) + { + if (coordinator.RequestReconfigurationLock()) + { + args.AllowDownload = true; + args.Callback = Browser_ConfigurationDownloadFinished; + args.DownloadPath = Path.Combine(context.AppConfig.TemporaryDirectory, fileName); + + splashScreen.Show(); + splashScreen.BringToForeground(); + splashScreen.SetIndeterminate(); + splashScreen.UpdateStatus(TextKey.OperationStatus_InitializeSession, true); + + logger.Info($"Allowed download request for configuration file '{fileName}'."); + } + else + { + logger.Warn($"A reconfiguration is already in progress, denied download request for configuration file '{fileName}'!"); + } + } + else + { + logger.Info($"Reconfiguration is not allowed, denied download request for configuration file '{fileName}'."); + } + } + + private bool IsAllowedToReconfigure(string url) { var allow = false; var hasQuitPassword = !string.IsNullOrWhiteSpace(Settings.Security.QuitPasswordHash); @@ -498,9 +531,9 @@ namespace SafeExamBrowser.Client { var expression = Regex.Escape(Settings.Security.ReconfigurationUrl).Replace(@"\*", ".*"); var regex = new Regex($"^{expression}$", RegexOptions.IgnoreCase); - var sebUrl = args.Url.Replace(Uri.UriSchemeHttps, context.AppConfig.SebUriSchemeSecure).Replace(Uri.UriSchemeHttp, context.AppConfig.SebUriScheme); + var sebUrl = url.Replace(Uri.UriSchemeHttps, context.AppConfig.SebUriSchemeSecure).Replace(Uri.UriSchemeHttp, context.AppConfig.SebUriScheme); - allow = Settings.Security.AllowReconfiguration && (regex.IsMatch(args.Url) || regex.IsMatch(sebUrl)); + allow = Settings.Security.AllowReconfiguration && (regex.IsMatch(url) || regex.IsMatch(sebUrl)); } else { @@ -512,24 +545,7 @@ namespace SafeExamBrowser.Client allow = Settings.ConfigurationMode == ConfigurationMode.ConfigureClient || Settings.Security.AllowReconfiguration; } - if (allow) - { - args.AllowDownload = true; - args.Callback = Browser_ConfigurationDownloadFinished; - args.DownloadPath = Path.Combine(context.AppConfig.TemporaryDirectory, fileName); - - splashScreen.Show(); - splashScreen.BringToForeground(); - splashScreen.SetIndeterminate(); - splashScreen.UpdateStatus(TextKey.OperationStatus_InitializeSession, true); - - logger.Info($"Allowed download request for configuration file '{fileName}'."); - } - else - { - args.AllowDownload = false; - logger.Info($"Denied download request for configuration file '{fileName}'."); - } + return allow; } private void Browser_ConfigurationDownloadFinished(bool success, string url, string filePath = null) @@ -547,15 +563,19 @@ namespace SafeExamBrowser.Client else { logger.Error($"Failed to communicate reconfiguration request for '{filePath}'!"); + messageBox.Show(TextKey.MessageBox_ReconfigurationError, TextKey.MessageBox_ReconfigurationErrorTitle, icon: MessageBoxIcon.Error, parent: splashScreen); splashScreen.Hide(); + coordinator.ReleaseReconfigurationLock(); } } else { logger.Error($"Failed to download configuration file '{filePath}'!"); + messageBox.Show(TextKey.MessageBox_ConfigurationDownloadError, TextKey.MessageBox_ConfigurationDownloadErrorTitle, icon: MessageBoxIcon.Error, parent: splashScreen); splashScreen.Hide(); + coordinator.ReleaseReconfigurationLock(); } } @@ -643,6 +663,7 @@ namespace SafeExamBrowser.Client { logger.Info("The reconfiguration was aborted by the runtime."); splashScreen.Hide(); + coordinator.ReleaseReconfigurationLock(); } private void ClientHost_ReconfigurationDenied(ReconfigurationEventArgs args) @@ -650,6 +671,7 @@ namespace SafeExamBrowser.Client logger.Info($"The reconfiguration request for '{args.ConfigurationPath}' was denied by the runtime!"); messageBox.Show(TextKey.MessageBox_ReconfigurationDenied, TextKey.MessageBox_ReconfigurationDeniedTitle, parent: splashScreen); splashScreen.Hide(); + coordinator.ReleaseReconfigurationLock(); } private void ClientHost_ServerFailureActionRequested(ServerFailureActionRequestEventArgs args) @@ -776,14 +798,13 @@ namespace SafeExamBrowser.Client { logger.Warn($@"The cursor registry value '{key}\{name}' has changed from '{oldValue}' to '{newValue}'! Attempting to show lock screen..."); - if (!sessionLocked) + if (coordinator.RequestSessionLock()) { var message = text.Get(TextKey.LockScreen_CursorMessage); var title = text.Get(TextKey.LockScreen_Title); var continueOption = new LockScreenOption { Text = text.Get(TextKey.LockScreen_CursorContinueOption) }; var terminateOption = new LockScreenOption { Text = text.Get(TextKey.LockScreen_CursorTerminateOption) }; - sessionLocked = true; registry.StopMonitoring(key, name); var result = ShowLockScreen(message, title, new[] { continueOption, terminateOption }); @@ -798,7 +819,7 @@ namespace SafeExamBrowser.Client TryRequestShutdown(); } - sessionLocked = false; + coordinator.ReleaseSessionLock(); } else { @@ -810,14 +831,13 @@ namespace SafeExamBrowser.Client { logger.Warn($@"The ease of access registry value '{key}\{name}' has changed from '{oldValue}' to '{newValue}'! Attempting to show lock screen..."); - if (!sessionLocked) + if (coordinator.RequestSessionLock()) { var message = text.Get(TextKey.LockScreen_EaseOfAccessMessage); var title = text.Get(TextKey.LockScreen_Title); var continueOption = new LockScreenOption { Text = text.Get(TextKey.LockScreen_EaseOfAccessContinueOption) }; var terminateOption = new LockScreenOption { Text = text.Get(TextKey.LockScreen_EaseOfAccessTerminateOption) }; - sessionLocked = true; registry.StopMonitoring(key, name); var result = ShowLockScreen(message, title, new[] { continueOption, terminateOption }); @@ -832,7 +852,7 @@ namespace SafeExamBrowser.Client TryRequestShutdown(); } - sessionLocked = false; + coordinator.ReleaseSessionLock(); } else { @@ -843,8 +863,8 @@ namespace SafeExamBrowser.Client private void Runtime_ConnectionLost() { logger.Error("Lost connection to the runtime!"); - messageBox.Show(TextKey.MessageBox_ApplicationError, TextKey.MessageBox_ApplicationErrorTitle, icon: MessageBoxIcon.Error); + messageBox.Show(TextKey.MessageBox_ApplicationError, TextKey.MessageBox_ApplicationErrorTitle, icon: MessageBoxIcon.Error); shutdown.Invoke(); } @@ -858,11 +878,10 @@ namespace SafeExamBrowser.Client { logger.Info("Attempting to show lock screen as requested by the server..."); - if (!sessionLocked) + if (coordinator.RequestSessionLock()) { - sessionLocked = true; ShowLockScreen(message, text.Get(TextKey.LockScreen_Title), Enumerable.Empty()); - sessionLocked = false; + coordinator.ReleaseSessionLock(); } else { @@ -900,10 +919,8 @@ namespace SafeExamBrowser.Client { logger.Warn("Detected user session change!"); - if (!sessionLocked) + if (coordinator.RequestSessionLock()) { - sessionLocked = true; - var result = ShowLockScreen(message, title, new[] { continueOption, terminateOption }); if (result.OptionId == terminateOption.Id) @@ -912,7 +929,7 @@ namespace SafeExamBrowser.Client TryRequestShutdown(); } - sessionLocked = false; + coordinator.ReleaseSessionLock(); } else { diff --git a/SafeExamBrowser.Client/CompositionRoot.cs b/SafeExamBrowser.Client/CompositionRoot.cs index f878c35c..d5bfbaa6 100644 --- a/SafeExamBrowser.Client/CompositionRoot.cs +++ b/SafeExamBrowser.Client/CompositionRoot.cs @@ -114,6 +114,7 @@ namespace SafeExamBrowser.Client var applicationFactory = new ApplicationFactory(applicationMonitor, ModuleLogger(nameof(ApplicationFactory)), nativeMethods, processFactory, new Registry(ModuleLogger(nameof(Registry)))); var clipboard = new Clipboard(ModuleLogger(nameof(Clipboard)), nativeMethods); + var coordinator = new Coordinator(); var displayMonitor = new DisplayMonitor(ModuleLogger(nameof(DisplayMonitor)), nativeMethods, systemInfo); var explorerShell = new ExplorerShell(ModuleLogger(nameof(ExplorerShell)), nativeMethods); var fileSystemDialog = BuildFileSystemDialog(); @@ -148,6 +149,7 @@ namespace SafeExamBrowser.Client actionCenter, applicationMonitor, context, + coordinator, displayMonitor, explorerShell, fileSystemDialog, diff --git a/SafeExamBrowser.Client/Contracts/ICoordinator.cs b/SafeExamBrowser.Client/Contracts/ICoordinator.cs new file mode 100644 index 00000000..d96eecd1 --- /dev/null +++ b/SafeExamBrowser.Client/Contracts/ICoordinator.cs @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024 ETH Zürich, IT Services + * + * 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.Client.Contracts +{ + /// + /// Coordinates concurrent operations of the client application. + /// + internal interface ICoordinator + { + /// + /// Indicates whether the reconfiguration lock is currently occupied. + /// + bool IsReconfigurationLocked(); + + /// + /// Indicates whether the session lock is currently occupied. + /// + bool IsSessionLocked(); + + /// + /// Releases the reconfiguration lock. + /// + void ReleaseReconfigurationLock(); + + /// + /// Releases the session lock. + /// + void ReleaseSessionLock(); + + /// + /// Attempts to acquire the unique reconfiguration lock. Returns true if successful, otherwise false. + /// + bool RequestReconfigurationLock(); + + /// + /// Attempts to acquire the unique session lock. Returns true if successful, otherwise false. + /// + bool RequestSessionLock(); + } +} diff --git a/SafeExamBrowser.Client/Coordinator.cs b/SafeExamBrowser.Client/Coordinator.cs new file mode 100644 index 00000000..50e5274a --- /dev/null +++ b/SafeExamBrowser.Client/Coordinator.cs @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2024 ETH Zürich, IT Services + * + * 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.Collections.Concurrent; +using SafeExamBrowser.Client.Contracts; + +namespace SafeExamBrowser.Client +{ + internal class Coordinator : ICoordinator + { + private readonly ConcurrentBag reconfiguration; + private readonly ConcurrentBag session; + + internal Coordinator() + { + reconfiguration = new ConcurrentBag(); + session = new ConcurrentBag(); + } + + public bool IsReconfigurationLocked() + { + return !reconfiguration.IsEmpty; + } + + public bool IsSessionLocked() + { + return !session.IsEmpty; + } + + public void ReleaseReconfigurationLock() + { + reconfiguration.TryTake(out _); + } + + public void ReleaseSessionLock() + { + session.TryTake(out _); + } + + public bool RequestReconfigurationLock() + { + var acquired = false; + + lock (reconfiguration) + { + if (reconfiguration.IsEmpty) + { + reconfiguration.Add(Guid.NewGuid()); + acquired = true; + } + } + + return acquired; + } + + public bool RequestSessionLock() + { + var acquired = false; + + lock (session) + { + if (session.IsEmpty) + { + session.Add(Guid.NewGuid()); + acquired = true; + } + } + + return acquired; + } + } +} diff --git a/SafeExamBrowser.Client/Properties/AssemblyInfo.cs b/SafeExamBrowser.Client/Properties/AssemblyInfo.cs index f908dafc..b194a3f8 100644 --- a/SafeExamBrowser.Client/Properties/AssemblyInfo.cs +++ b/SafeExamBrowser.Client/Properties/AssemblyInfo.cs @@ -16,6 +16,9 @@ using System.Windows; // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] + +// Required for mocking internal contracts with Moq +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] [assembly: InternalsVisibleTo("SafeExamBrowser.Client.UnitTests")] //In order to begin building localizable applications, set diff --git a/SafeExamBrowser.Client/SafeExamBrowser.Client.csproj b/SafeExamBrowser.Client/SafeExamBrowser.Client.csproj index 16f99a04..47c83612 100644 --- a/SafeExamBrowser.Client/SafeExamBrowser.Client.csproj +++ b/SafeExamBrowser.Client/SafeExamBrowser.Client.csproj @@ -74,6 +74,8 @@ + +