SEBWIN-771: Implemented reconfiguration safeguard and refactored and moved reconfiguration and session locking to new coordinator module.

This commit is contained in:
Damian Büchel 2024-07-15 18:34:30 +02:00
parent f3a9030505
commit b48ef21708
9 changed files with 364 additions and 41 deletions

View file

@ -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<IBrowserApplication> browser;
private Mock<IClientHost> clientHost;
private ClientContext context;
private Mock<ICoordinator> coordinator;
private Mock<IDisplayMonitor> displayMonitor;
private Mock<IExplorerShell> explorerShell;
private Mock<IFileSystemDialog> fileSystemDialog;
@ -90,6 +92,7 @@ namespace SafeExamBrowser.Client.UnitTests
browser = new Mock<IBrowserApplication>();
clientHost = new Mock<IClientHost>();
context = new ClientContext();
coordinator = new Mock<ICoordinator>();
displayMonitor = new Mock<IDisplayMonitor>();
explorerShell = new Mock<IExplorerShell>();
fileSystemDialog = new Mock<IFileSystemDialog>();
@ -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<string>(), It.IsAny<string>())).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<string>(), It.IsAny<string>()), 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<string>(), It.IsAny<string>()), 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<string>(), It.IsAny<string>())).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<string>(), It.IsAny<string>()), 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<string>(), It.IsAny<string>()), 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<string>(), It.IsAny<string>()), 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<string>(), It.IsAny<string>()), 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<TextKey>(),
It.IsAny<TextKey>(),
@ -825,11 +866,14 @@ namespace SafeExamBrowser.Client.UnitTests
runtimeProxy.Setup(r => r.RequestReconfiguration(
It.Is<string>(p => p == downloadPath),
It.Is<string>(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<string>(p => p == downloadPath), It.Is<string>(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<TextKey>(),
It.IsAny<TextKey>(),
@ -855,11 +899,14 @@ namespace SafeExamBrowser.Client.UnitTests
runtimeProxy.Setup(r => r.RequestReconfiguration(
It.Is<string>(p => p == downloadPath),
It.Is<string>(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<string>(), It.IsAny<string>()), 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<TextKey>(),
It.IsAny<TextKey>(),
@ -882,18 +929,21 @@ namespace SafeExamBrowser.Client.UnitTests
runtimeProxy.Setup(r => r.RequestReconfiguration(
It.Is<string>(p => p == downloadPath),
It.Is<string>(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<string>(), It.IsAny<string>()), Times.Once);
coordinator.Verify(c => c.RequestReconfigurationLock(), Times.Once);
coordinator.Verify(c => c.ReleaseReconfigurationLock(), Times.Once);
messageBox.Verify(m => m.Show(
It.IsAny<TextKey>(),
It.IsAny<TextKey>(),
It.IsAny<MessageBoxAction>(),
It.Is<MessageBoxIcon>(i => i == MessageBoxIcon.Error),
It.IsAny<IWindow>()), Times.Once);
runtimeProxy.Verify(r => r.RequestReconfiguration(It.IsAny<string>(), It.IsAny<string>()), Times.Once);
}
[TestMethod]
@ -1231,8 +1281,9 @@ namespace SafeExamBrowser.Client.UnitTests
{
var lockScreen = new Mock<ILockScreen>();
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<string>(), It.IsAny<string>(), It.IsAny<IEnumerable<LockScreenOption>>(), It.IsAny<LockScreenSettings>()))
.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<ILockScreen>();
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<string>(), It.IsAny<string>(), It.IsAny<IEnumerable<LockScreenOption>>(), It.IsAny<LockScreenSettings>()))
.Callback(new Action<string, string, IEnumerable<LockScreenOption>, 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);
}

View file

@ -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);
}
}
}

View file

@ -166,6 +166,7 @@
<Compile Include="Operations\SystemMonitorOperationTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ClientControllerTests.cs" />
<Compile Include="CoordinatorTests.cs" />
</ItemGroup>
<ItemGroup>
<None Include="app.config">

View file

@ -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<LockScreenOption>());
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
{

View file

@ -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,

View file

@ -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
{
/// <summary>
/// Coordinates concurrent operations of the client application.
/// </summary>
internal interface ICoordinator
{
/// <summary>
/// Indicates whether the reconfiguration lock is currently occupied.
/// </summary>
bool IsReconfigurationLocked();
/// <summary>
/// Indicates whether the session lock is currently occupied.
/// </summary>
bool IsSessionLocked();
/// <summary>
/// Releases the reconfiguration lock.
/// </summary>
void ReleaseReconfigurationLock();
/// <summary>
/// Releases the session lock.
/// </summary>
void ReleaseSessionLock();
/// <summary>
/// Attempts to acquire the unique reconfiguration lock. Returns <c>true</c> if successful, otherwise <c>false</c>.
/// </summary>
bool RequestReconfigurationLock();
/// <summary>
/// Attempts to acquire the unique session lock. Returns <c>true</c> if successful, otherwise <c>false</c>.
/// </summary>
bool RequestSessionLock();
}
}

View file

@ -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<Guid> reconfiguration;
private readonly ConcurrentBag<Guid> session;
internal Coordinator()
{
reconfiguration = new ConcurrentBag<Guid>();
session = new ConcurrentBag<Guid>();
}
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;
}
}
}

View file

@ -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

View file

@ -74,6 +74,8 @@
<Compile Include="App.cs" />
<Compile Include="ClientContext.cs" />
<Compile Include="ClientController.cs" />
<Compile Include="Contracts\ICoordinator.cs" />
<Compile Include="Coordinator.cs" />
<Compile Include="Operations\ClientHostDisconnectionOperation.cs" />
<Compile Include="Operations\ClientOperation.cs" />
<Compile Include="Operations\ConfigurationOperation.cs" />