diff --git a/SafeExamBrowser.Contracts/I18n/TextKey.cs b/SafeExamBrowser.Contracts/I18n/TextKey.cs index 0c1cacdc..9a8ca5e7 100644 --- a/SafeExamBrowser.Contracts/I18n/TextKey.cs +++ b/SafeExamBrowser.Contracts/I18n/TextKey.cs @@ -91,6 +91,7 @@ namespace SafeExamBrowser.Contracts.I18n OperationStatus_StopWindowMonitoring, OperationStatus_TerminateBrowser, OperationStatus_TerminateShell, + OperationStatus_UpdateSystemConfiguration, OperationStatus_WaitExplorerStartup, OperationStatus_WaitExplorerTermination, OperationStatus_WaitRuntimeDisconnection, diff --git a/SafeExamBrowser.Contracts/Lockdown/ISystemConfigurationUpdate.cs b/SafeExamBrowser.Contracts/Lockdown/ISystemConfigurationUpdate.cs new file mode 100644 index 00000000..08b7987c --- /dev/null +++ b/SafeExamBrowser.Contracts/Lockdown/ISystemConfigurationUpdate.cs @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2019 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.Contracts.Lockdown +{ + /// + /// Provides functionality to update and enforce the system configuration. + /// + public interface ISystemConfigurationUpdate + { + /// + /// Executes the update synchronously. + /// + void Execute(); + + /// + /// Executes the udpate asynchronously. + /// + void ExecuteAsync(); + } +} diff --git a/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj b/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj index 880ad5ce..6a6f9df0 100644 --- a/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj +++ b/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj @@ -96,6 +96,7 @@ + diff --git a/SafeExamBrowser.I18n/Text.xml b/SafeExamBrowser.I18n/Text.xml index d98e523e..68fc4fb0 100644 --- a/SafeExamBrowser.I18n/Text.xml +++ b/SafeExamBrowser.I18n/Text.xml @@ -231,6 +231,9 @@ Terminating user interface + + Updating system configuration + Waiting for Windows explorer to start up diff --git a/SafeExamBrowser.Lockdown.UnitTests/AutoRestoreMechanismTests.cs b/SafeExamBrowser.Lockdown.UnitTests/AutoRestoreMechanismTests.cs index 7b9d15db..15d20107 100644 --- a/SafeExamBrowser.Lockdown.UnitTests/AutoRestoreMechanismTests.cs +++ b/SafeExamBrowser.Lockdown.UnitTests/AutoRestoreMechanismTests.cs @@ -21,6 +21,7 @@ namespace SafeExamBrowser.Lockdown.UnitTests { private Mock backup; private Mock logger; + private Mock systemConfigurationUpdate; private AutoRestoreMechanism sut; [TestInitialize] @@ -28,8 +29,9 @@ namespace SafeExamBrowser.Lockdown.UnitTests { backup = new Mock(); logger = new Mock(); + systemConfigurationUpdate = new Mock(); - sut = new AutoRestoreMechanism(backup.Object, logger.Object, 0); + sut = new AutoRestoreMechanism(backup.Object, logger.Object, systemConfigurationUpdate.Object, 0); } [TestMethod] @@ -78,17 +80,20 @@ namespace SafeExamBrowser.Lockdown.UnitTests var list = new List { configuration.Object }; var sync = new AutoResetEvent(false); - backup.Setup(b => b.GetAllConfigurations()).Returns(list).Callback(() => counter++); - backup.Setup(b => b.Delete(It.IsAny())).Callback(() => { list.Clear(); sync.Set(); }); - configuration.Setup(c => c.Restore()).Returns(() => counter == limit); + backup.Setup(b => b.GetAllConfigurations()).Returns(() => new List(list)).Callback(() => counter++); + backup.Setup(b => b.Delete(It.IsAny())).Callback(() => list.Clear()); + configuration.Setup(c => c.Restore()).Returns(() => counter >= limit); + systemConfigurationUpdate.Setup(u => u.ExecuteAsync()).Callback(() => sync.Set()); sut.Start(); sync.WaitOne(); sut.Stop(); - backup.Verify(b => b.GetAllConfigurations(), Times.Exactly(limit)); + backup.Verify(b => b.GetAllConfigurations(), Times.Exactly(limit + 1)); backup.Verify(b => b.Delete(It.Is(c => c == configuration.Object)), Times.Once); configuration.Verify(c => c.Restore(), Times.Exactly(limit)); + systemConfigurationUpdate.Verify(u => u.Execute(), Times.Never); + systemConfigurationUpdate.Verify(u => u.ExecuteAsync(), Times.Once); } [TestMethod] @@ -103,7 +108,7 @@ namespace SafeExamBrowser.Lockdown.UnitTests var list = new List { configuration.Object }; var sync = new AutoResetEvent(false); - sut = new AutoRestoreMechanism(backup.Object, logger.Object, TIMEOUT); + sut = new AutoRestoreMechanism(backup.Object, logger.Object, systemConfigurationUpdate.Object, TIMEOUT); backup.Setup(b => b.GetAllConfigurations()).Returns(list).Callback(() => { @@ -144,9 +149,10 @@ namespace SafeExamBrowser.Lockdown.UnitTests } [TestMethod] + [ExpectedException(typeof(ArgumentException))] public void MustValidateTimeout() { - Assert.ThrowsException(() => new AutoRestoreMechanism(backup.Object, logger.Object, new Random().Next(int.MinValue, -1))); + new AutoRestoreMechanism(backup.Object, logger.Object, systemConfigurationUpdate.Object, new Random().Next(int.MinValue, -1)); } } } diff --git a/SafeExamBrowser.Lockdown/AutoRestoreMechanism.cs b/SafeExamBrowser.Lockdown/AutoRestoreMechanism.cs index fc9d7b40..182f70ec 100644 --- a/SafeExamBrowser.Lockdown/AutoRestoreMechanism.cs +++ b/SafeExamBrowser.Lockdown/AutoRestoreMechanism.cs @@ -20,10 +20,15 @@ namespace SafeExamBrowser.Lockdown private IFeatureConfigurationBackup backup; private ILogger logger; + private ISystemConfigurationUpdate systemConfigurationUpdate; private bool running; private int timeout_ms; - public AutoRestoreMechanism(IFeatureConfigurationBackup backup, ILogger logger, int timeout_ms) + public AutoRestoreMechanism( + IFeatureConfigurationBackup backup, + ILogger logger, + ISystemConfigurationUpdate systemConfigurationUpdate, + int timeout_ms) { if (timeout_ms < 0) { @@ -32,6 +37,7 @@ namespace SafeExamBrowser.Lockdown this.backup = backup; this.logger = logger; + this.systemConfigurationUpdate = systemConfigurationUpdate; this.timeout_ms = timeout_ms; } @@ -71,42 +77,53 @@ namespace SafeExamBrowser.Lockdown private void RestoreAll() { var configurations = backup.GetAllConfigurations(); + var all = configurations.Count; + var restored = 0; - if (!configurations.Any()) + if (configurations.Any()) { - running = false; - logger.Info("Nothing to restore, stopped auto-restore mechanism."); + logger.Info($"Attempting to restore {configurations.Count} items..."); - return; - } - - logger.Info($"Attempting to restore {configurations.Count} items..."); - - foreach (var configuration in configurations) - { - var success = configuration.Restore(); - - if (success) + foreach (var configuration in configurations) { - backup.Delete(configuration); - } - else - { - logger.Warn($"Failed to restore {configuration}!"); - } + var success = configuration.Restore(); - lock (@lock) - { - if (!running) + if (success) { - logger.Info("Auto-restore mechanism was aborted."); + backup.Delete(configuration); + restored++; + } + else + { + logger.Warn($"Failed to restore {configuration}!"); + } - return; + lock (@lock) + { + if (!running) + { + logger.Info("Auto-restore mechanism was aborted."); + + return; + } } } - } - Task.Delay(timeout_ms).ContinueWith((_) => RestoreAll()); + if (all == restored) + { + systemConfigurationUpdate.ExecuteAsync(); + } + + Task.Delay(timeout_ms).ContinueWith((_) => RestoreAll()); + } + else + { + lock (@lock) + { + running = false; + logger.Info("Nothing to restore, stopped auto-restore mechanism."); + } + } } } } diff --git a/SafeExamBrowser.Lockdown/SafeExamBrowser.Lockdown.csproj b/SafeExamBrowser.Lockdown/SafeExamBrowser.Lockdown.csproj index 50e185f8..8ce3b927 100644 --- a/SafeExamBrowser.Lockdown/SafeExamBrowser.Lockdown.csproj +++ b/SafeExamBrowser.Lockdown/SafeExamBrowser.Lockdown.csproj @@ -75,6 +75,7 @@ + diff --git a/SafeExamBrowser.Lockdown/SystemConfigurationUpdate.cs b/SafeExamBrowser.Lockdown/SystemConfigurationUpdate.cs new file mode 100644 index 00000000..79f9e70b --- /dev/null +++ b/SafeExamBrowser.Lockdown/SystemConfigurationUpdate.cs @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2019 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.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using SafeExamBrowser.Contracts.Lockdown; +using SafeExamBrowser.Contracts.Logging; + +namespace SafeExamBrowser.Lockdown +{ + public class SystemConfigurationUpdate : ISystemConfigurationUpdate + { + private ILogger logger; + + public SystemConfigurationUpdate(ILogger logger) + { + this.logger = logger; + } + + public void Execute() + { + try + { + logger.Info("Starting system configuration update..."); + + var process = Process.Start(new ProcessStartInfo("cmd.exe", "/c \"gpupdate /force\"") + { + CreateNoWindow = true, + RedirectStandardOutput = true, + UseShellExecute = false + }); + + logger.Info("Waiting for update to complete..."); + process.WaitForExit(); + + var output = process.StandardOutput.ReadToEnd(); + var lines = output.Split(new [] { Environment.NewLine, "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries); + + logger.Info($"Update has completed: {String.Join(" ", lines.Skip(1))}"); + } + catch (Exception e) + { + logger.Error("Failed to update system configuration!", e); + } + } + + public void ExecuteAsync() + { + Task.Run(new Action(Execute)); + } + } +} diff --git a/SafeExamBrowser.Runtime.UnitTests/Operations/ServiceOperationTests.cs b/SafeExamBrowser.Runtime.UnitTests/Operations/ServiceOperationTests.cs index 698cbd80..7acc5236 100644 --- a/SafeExamBrowser.Runtime.UnitTests/Operations/ServiceOperationTests.cs +++ b/SafeExamBrowser.Runtime.UnitTests/Operations/ServiceOperationTests.cs @@ -15,6 +15,7 @@ using SafeExamBrowser.Contracts.Communication.Proxies; using SafeExamBrowser.Contracts.Configuration; using SafeExamBrowser.Contracts.Configuration.Settings; using SafeExamBrowser.Contracts.Core.OperationModel; +using SafeExamBrowser.Contracts.Lockdown; using SafeExamBrowser.Contracts.Logging; using SafeExamBrowser.Contracts.SystemComponents; using SafeExamBrowser.Contracts.UserInterface.MessageBox; @@ -34,6 +35,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations private SessionConfiguration session; private SessionContext sessionContext; private Settings settings; + private Mock systemConfigurationUpdate; private Mock userInfo; private ServiceOperation sut; @@ -50,6 +52,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations session = new SessionConfiguration(); sessionContext = new SessionContext(); settings = new Settings(); + systemConfigurationUpdate = new Mock(); userInfo = new Mock(); appConfig.ServiceEventName = serviceEventName; @@ -60,7 +63,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations session.Settings = settings; settings.Service.Policy = ServicePolicy.Mandatory; - sut = new ServiceOperation(logger.Object, runtimeHost.Object, service.Object, sessionContext, 0, userInfo.Object); + sut = new ServiceOperation(logger.Object, runtimeHost.Object, service.Object, sessionContext, systemConfigurationUpdate.Object, 0, userInfo.Object); } [TestMethod] @@ -89,6 +92,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations var result = sut.Perform(); service.Verify(s => s.StartSession(It.IsAny()), Times.Once); + systemConfigurationUpdate.Verify(u => u.Execute(), Times.Once); userInfo.Verify(u => u.GetUserName(), Times.Once); userInfo.Verify(u => u.GetUserSid(), Times.Once); @@ -121,7 +125,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations service.Setup(s => s.Connect(null, true)).Returns(true); service.Setup(s => s.StartSession(It.IsAny())).Returns(new CommunicationResult(true)); - sut = new ServiceOperation(logger.Object, runtimeHost.Object, service.Object, sessionContext, TIMEOUT, userInfo.Object); + sut = new ServiceOperation(logger.Object, runtimeHost.Object, service.Object, sessionContext, systemConfigurationUpdate.Object, TIMEOUT, userInfo.Object); before = DateTime.Now; var result = sut.Perform(); @@ -154,6 +158,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations var result = sut.Perform(); service.Verify(s => s.StartSession(It.IsAny()), Times.Once); + systemConfigurationUpdate.Verify(u => u.Execute(), Times.Never); Assert.AreEqual(OperationResult.Failed, result); } @@ -207,8 +212,8 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations public void Repeat_MustStopCurrentAndStartNewSession() { service.Setup(s => s.StopSession(It.IsAny())).Returns(new CommunicationResult(true)).Callback(() => serviceEvent.Set()); - PerformNormally(); + systemConfigurationUpdate.Reset(); var result = sut.Repeat(); @@ -216,6 +221,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations service.Verify(s => s.StopSession(It.IsAny()), Times.Once); service.Verify(s => s.StartSession(It.IsAny()), Times.Exactly(2)); service.Verify(s => s.Disconnect(), Times.Never); + systemConfigurationUpdate.Verify(u => u.Execute(), Times.Exactly(2)); Assert.AreEqual(OperationResult.Success, result); } @@ -262,7 +268,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations var before = default(DateTime); service.Setup(s => s.StopSession(It.IsAny())).Returns(new CommunicationResult(true)); - sut = new ServiceOperation(logger.Object, runtimeHost.Object, service.Object, sessionContext, TIMEOUT, userInfo.Object); + sut = new ServiceOperation(logger.Object, runtimeHost.Object, service.Object, sessionContext, systemConfigurationUpdate.Object, TIMEOUT, userInfo.Object); PerformNormally(); @@ -297,13 +303,14 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations { service.Setup(s => s.Disconnect()).Returns(true); service.Setup(s => s.StopSession(It.IsAny())).Returns(new CommunicationResult(true)).Callback(() => serviceEvent.Set()); - PerformNormally(); + systemConfigurationUpdate.Reset(); var result = sut.Revert(); service.Verify(s => s.StopSession(It.IsAny()), Times.Once); service.Verify(s => s.Disconnect(), Times.Once); + systemConfigurationUpdate.Verify(u => u.Execute(), Times.Once); Assert.AreEqual(OperationResult.Success, result); } @@ -312,13 +319,14 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations public void Revert_MustHandleCommunicationFailureWhenStoppingSession() { service.Setup(s => s.StopSession(It.IsAny())).Returns(new CommunicationResult(false)); - PerformNormally(); + systemConfigurationUpdate.Reset(); var result = sut.Revert(); service.Verify(s => s.StopSession(It.IsAny()), Times.Once); service.Verify(s => s.Disconnect(), Times.Once); + systemConfigurationUpdate.Verify(u => u.Execute(), Times.Never); Assert.AreEqual(OperationResult.Failed, result); } @@ -344,7 +352,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations var before = default(DateTime); service.Setup(s => s.StopSession(It.IsAny())).Returns(new CommunicationResult(true)); - sut = new ServiceOperation(logger.Object, runtimeHost.Object, service.Object, sessionContext, TIMEOUT, userInfo.Object); + sut = new ServiceOperation(logger.Object, runtimeHost.Object, service.Object, sessionContext, systemConfigurationUpdate.Object, TIMEOUT, userInfo.Object); PerformNormally(); diff --git a/SafeExamBrowser.Runtime/CompositionRoot.cs b/SafeExamBrowser.Runtime/CompositionRoot.cs index c690932d..5a38d7f6 100644 --- a/SafeExamBrowser.Runtime/CompositionRoot.cs +++ b/SafeExamBrowser.Runtime/CompositionRoot.cs @@ -27,6 +27,7 @@ using SafeExamBrowser.Contracts.SystemComponents; using SafeExamBrowser.Core.OperationModel; using SafeExamBrowser.Core.Operations; using SafeExamBrowser.I18n; +using SafeExamBrowser.Lockdown; using SafeExamBrowser.Logging; using SafeExamBrowser.Runtime.Communication; using SafeExamBrowser.Runtime.Operations; @@ -70,6 +71,7 @@ namespace SafeExamBrowser.Runtime var runtimeHost = new RuntimeHost(appConfig.RuntimeAddress, new HostObjectFactory(), ModuleLogger(nameof(RuntimeHost)), FIVE_SECONDS); var serviceProxy = new ServiceProxy(appConfig.ServiceAddress, new ProxyObjectFactory(), ModuleLogger(nameof(ServiceProxy)), Interlocutor.Runtime); var sessionContext = new SessionContext(); + var systemConfigurationUpdate = new SystemConfigurationUpdate(ModuleLogger(nameof(SystemConfigurationUpdate))); var uiFactory = new UserInterfaceFactory(text); var userInfo = new UserInfo(); @@ -81,7 +83,7 @@ namespace SafeExamBrowser.Runtime sessionOperations.Enqueue(new SessionInitializationOperation(configuration, logger, runtimeHost, sessionContext)); sessionOperations.Enqueue(new ConfigurationOperation(args, configuration, new HashAlgorithm(), logger, sessionContext)); - sessionOperations.Enqueue(new ServiceOperation(logger, runtimeHost, serviceProxy, sessionContext, THIRTY_SECONDS, userInfo)); + sessionOperations.Enqueue(new ServiceOperation(logger, runtimeHost, serviceProxy, sessionContext, systemConfigurationUpdate, THIRTY_SECONDS, userInfo)); sessionOperations.Enqueue(new ClientTerminationOperation(logger, processFactory, proxyFactory, runtimeHost, sessionContext, THIRTY_SECONDS)); sessionOperations.Enqueue(new KioskModeOperation(desktopFactory, explorerShell, logger, processFactory, sessionContext)); sessionOperations.Enqueue(new ClientOperation(logger, processFactory, proxyFactory, runtimeHost, sessionContext, THIRTY_SECONDS)); diff --git a/SafeExamBrowser.Runtime/Operations/ServiceOperation.cs b/SafeExamBrowser.Runtime/Operations/ServiceOperation.cs index 38fc5140..ff17a004 100644 --- a/SafeExamBrowser.Runtime/Operations/ServiceOperation.cs +++ b/SafeExamBrowser.Runtime/Operations/ServiceOperation.cs @@ -16,6 +16,7 @@ using SafeExamBrowser.Contracts.Configuration.Settings; using SafeExamBrowser.Contracts.Core.OperationModel; using SafeExamBrowser.Contracts.Core.OperationModel.Events; using SafeExamBrowser.Contracts.I18n; +using SafeExamBrowser.Contracts.Lockdown; using SafeExamBrowser.Contracts.Logging; using SafeExamBrowser.Contracts.SystemComponents; using SafeExamBrowser.Contracts.UserInterface.MessageBox; @@ -28,6 +29,7 @@ namespace SafeExamBrowser.Runtime.Operations private ILogger logger; private IRuntimeHost runtimeHost; private IServiceProxy service; + private ISystemConfigurationUpdate systemConfigurationUpdate; private int timeout_ms; private IUserInfo userInfo; @@ -39,12 +41,14 @@ namespace SafeExamBrowser.Runtime.Operations IRuntimeHost runtimeHost, IServiceProxy service, SessionContext sessionContext, + ISystemConfigurationUpdate systemConfigurationUpdate, int timeout_ms, IUserInfo userInfo) : base(sessionContext) { this.logger = logger; this.runtimeHost = runtimeHost; this.service = service; + this.systemConfigurationUpdate = systemConfigurationUpdate; this.timeout_ms = timeout_ms; this.userInfo = userInfo; } @@ -188,6 +192,9 @@ namespace SafeExamBrowser.Runtime.Operations { logger.Error($"Failed to start new service session within {timeout_ms / 1000} seconds!"); } + + StatusChanged?.Invoke(TextKey.OperationStatus_UpdateSystemConfiguration); + systemConfigurationUpdate.Execute(); } else { @@ -217,6 +224,9 @@ namespace SafeExamBrowser.Runtime.Operations { logger.Error($"Failed to stop service session within {timeout_ms / 1000} seconds!"); } + + StatusChanged?.Invoke(TextKey.OperationStatus_UpdateSystemConfiguration); + systemConfigurationUpdate.Execute(); } else { diff --git a/SafeExamBrowser.Runtime/SafeExamBrowser.Runtime.csproj b/SafeExamBrowser.Runtime/SafeExamBrowser.Runtime.csproj index 0951b584..677f60af 100644 --- a/SafeExamBrowser.Runtime/SafeExamBrowser.Runtime.csproj +++ b/SafeExamBrowser.Runtime/SafeExamBrowser.Runtime.csproj @@ -155,6 +155,10 @@ {10c62628-8e6a-45aa-9d97-339b119ad21d} SafeExamBrowser.I18n + + {386b6042-3e12-4753-9fc6-c88ea4f97030} + SafeExamBrowser.Lockdown + {e107026c-2011-4552-a7d8-3a0d37881df6} SafeExamBrowser.Logging diff --git a/SafeExamBrowser.Service/CompositionRoot.cs b/SafeExamBrowser.Service/CompositionRoot.cs index 144373d5..13caeecb 100644 --- a/SafeExamBrowser.Service/CompositionRoot.cs +++ b/SafeExamBrowser.Service/CompositionRoot.cs @@ -46,11 +46,12 @@ namespace SafeExamBrowser.Service var proxyFactory = new ProxyFactory(new ProxyObjectFactory(), new ModuleLogger(logger, nameof(ProxyFactory))); var serviceHost = new ServiceHost(AppConfig.SERVICE_ADDRESS, new HostObjectFactory(), new ModuleLogger(logger, nameof(ServiceHost)), FIVE_SECONDS); var sessionContext = new SessionContext(); + var systemConfigurationUpdate = new SystemConfigurationUpdate(new ModuleLogger(logger, nameof(SystemConfigurationUpdate))); var bootstrapOperations = new Queue(); var sessionOperations = new Queue(); - sessionContext.AutoRestoreMechanism = new AutoRestoreMechanism(featureBackup, new ModuleLogger(logger, nameof(AutoRestoreMechanism)), FIVE_SECONDS); + sessionContext.AutoRestoreMechanism = new AutoRestoreMechanism(featureBackup, new ModuleLogger(logger, nameof(AutoRestoreMechanism)), systemConfigurationUpdate, FIVE_SECONDS); bootstrapOperations.Enqueue(new RestoreOperation(featureBackup, logger, sessionContext)); bootstrapOperations.Enqueue(new CommunicationHostOperation(serviceHost, logger));