diff --git a/SafeExamBrowser.Contracts/Lockdown/FeatureConfigurationStatus.cs b/SafeExamBrowser.Contracts/Lockdown/FeatureConfigurationStatus.cs new file mode 100644 index 00000000..5e6ea694 --- /dev/null +++ b/SafeExamBrowser.Contracts/Lockdown/FeatureConfigurationStatus.cs @@ -0,0 +1,31 @@ +/* + * 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 +{ + /// + /// Defines all possible states of an . + /// + public enum FeatureConfigurationStatus + { + /// + /// The configuration is in an undefined state. + /// + Undefined, + + /// + /// The configuration is disabled. + /// + Disabled, + + /// + /// The configuration is enabled. + /// + Enabled + } +} diff --git a/SafeExamBrowser.Contracts/Lockdown/IFeatureConfiguration.cs b/SafeExamBrowser.Contracts/Lockdown/IFeatureConfiguration.cs index f525d062..d2153b4d 100644 --- a/SafeExamBrowser.Contracts/Lockdown/IFeatureConfiguration.cs +++ b/SafeExamBrowser.Contracts/Lockdown/IFeatureConfiguration.cs @@ -35,16 +35,16 @@ namespace SafeExamBrowser.Contracts.Lockdown /// bool EnableFeature(); + /// + /// Retrieves the current status of the configuration. + /// + FeatureConfigurationStatus GetStatus(); + /// /// Initializes the currently active configuration of the feature. /// void Initialize(); - /// - /// Starts monitoring the feature to ensure that it remains as currently configured. - /// - void Monitor(); - /// /// Restores the feature to its previous configuration (i.e. before it was enabled or disabled). Returns true if successful, /// otherwise false. diff --git a/SafeExamBrowser.Contracts/Lockdown/IFeatureConfigurationMonitor.cs b/SafeExamBrowser.Contracts/Lockdown/IFeatureConfigurationMonitor.cs new file mode 100644 index 00000000..8c7e97f1 --- /dev/null +++ b/SafeExamBrowser.Contracts/Lockdown/IFeatureConfigurationMonitor.cs @@ -0,0 +1,31 @@ +/* + * 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 ensure that one or more remain in a given . + /// + public interface IFeatureConfigurationMonitor + { + /// + /// Registers a configuration to be monitored for the given status. + /// + void Observe(IFeatureConfiguration configuration, FeatureConfigurationStatus status); + + /// + /// Stops the monitoring activity and removes all observed configurations. + /// + void Reset(); + + /// + /// Starts the monitoring activity. + /// + void Start(); + } +} diff --git a/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj b/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj index 6a6f9df0..258ebc73 100644 --- a/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj +++ b/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj @@ -92,10 +92,12 @@ + + diff --git a/SafeExamBrowser.Lockdown.UnitTests/AutoRestoreMechanismTests.cs b/SafeExamBrowser.Lockdown.UnitTests/AutoRestoreMechanismTests.cs index 15d20107..7295bbb1 100644 --- a/SafeExamBrowser.Lockdown.UnitTests/AutoRestoreMechanismTests.cs +++ b/SafeExamBrowser.Lockdown.UnitTests/AutoRestoreMechanismTests.cs @@ -89,7 +89,7 @@ namespace SafeExamBrowser.Lockdown.UnitTests sync.WaitOne(); sut.Stop(); - backup.Verify(b => b.GetAllConfigurations(), Times.Exactly(limit + 1)); + backup.Verify(b => b.GetAllConfigurations(), Times.Exactly(limit)); 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); @@ -145,7 +145,7 @@ namespace SafeExamBrowser.Lockdown.UnitTests Thread.Sleep(25); sut.Stop(); - backup.Verify(b => b.GetAllConfigurations(), Times.Between(counter, counter + 1, Range.Inclusive)); + backup.Verify(b => b.GetAllConfigurations(), Times.Exactly(counter)); } [TestMethod] diff --git a/SafeExamBrowser.Lockdown.UnitTests/FeatureConfigurationMonitorTests.cs b/SafeExamBrowser.Lockdown.UnitTests/FeatureConfigurationMonitorTests.cs new file mode 100644 index 00000000..dcfac3ba --- /dev/null +++ b/SafeExamBrowser.Lockdown.UnitTests/FeatureConfigurationMonitorTests.cs @@ -0,0 +1,196 @@ +/* + * 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.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using SafeExamBrowser.Contracts.Lockdown; +using SafeExamBrowser.Contracts.Logging; + +namespace SafeExamBrowser.Lockdown.UnitTests +{ + [TestClass] + public class FeatureConfigurationMonitorTests + { + private Mock logger; + private FeatureConfigurationMonitor sut; + + [TestInitialize] + public void Initialize() + { + logger = new Mock(); + sut = new FeatureConfigurationMonitor(logger.Object, 0); + } + + [TestMethod] + public void MustEnforceConfigurations() + { + var configuration1 = new Mock(); + var configuration2 = new Mock(); + var configuration3 = new Mock(); + var counter = 0; + var limit = new Random().Next(5, 50); + var sync = new AutoResetEvent(false); + + configuration1.Setup(c => c.GetStatus()).Returns(FeatureConfigurationStatus.Disabled); + configuration2.Setup(c => c.GetStatus()).Returns(FeatureConfigurationStatus.Enabled); + configuration3.Setup(c => c.GetStatus()).Returns(FeatureConfigurationStatus.Disabled).Callback(() => + { + if (++counter >= limit) + { + sync.Set(); + } + }); + + sut = new FeatureConfigurationMonitor(logger.Object, 2); + + sut.Observe(configuration1.Object, FeatureConfigurationStatus.Enabled); + sut.Observe(configuration2.Object, FeatureConfigurationStatus.Disabled); + sut.Observe(configuration3.Object, FeatureConfigurationStatus.Undefined); + sut.Start(); + sync.WaitOne(); + sut.Reset(); + + configuration1.Verify(c => c.EnableFeature(), Times.Exactly(limit)); + configuration2.Verify(c => c.DisableFeature(), Times.Exactly(limit)); + configuration3.Verify(c => c.DisableFeature(), Times.Never); + configuration3.Verify(c => c.EnableFeature(), Times.Never); + } + + [TestMethod] + public void MustExecuteAsynchronously() + { + var configuration = new Mock(); + var sync = new AutoResetEvent(false); + var threadId = Thread.CurrentThread.ManagedThreadId; + + configuration.Setup(c => c.GetStatus()).Callback(() => { threadId = Thread.CurrentThread.ManagedThreadId; sync.Set(); }); + + sut.Observe(configuration.Object, FeatureConfigurationStatus.Disabled); + sut.Start(); + sync.WaitOne(); + sut.Reset(); + + Assert.AreNotEqual(Thread.CurrentThread.ManagedThreadId, threadId); + } + + [TestMethod] + public void MustNotStartMultipleTimes() + { + var configuration = new Mock(); + var counter = 0; + var sync = new AutoResetEvent(false); + + configuration.Setup(c => c.GetStatus()).Returns(() => + { + counter++; + Thread.Sleep(50); + sync.Set(); + sut.Reset(); + + return FeatureConfigurationStatus.Disabled; + }); + + sut.Observe(configuration.Object, FeatureConfigurationStatus.Enabled); + sut.Start(); + sut.Start(); + sut.Start(); + sut.Start(); + sut.Start(); + sut.Start(); + sut.Start(); + sync.WaitOne(); + sut.Reset(); + + Assert.AreEqual(1, counter); + } + + [TestMethod] + public void MustRespectTimeout() + { + const int TIMEOUT = 50; + + var after = default(DateTime); + var before = default(DateTime); + var configuration = new Mock(); + var counter = 0; + var sync = new AutoResetEvent(false); + + sut = new FeatureConfigurationMonitor(logger.Object, TIMEOUT); + + configuration.Setup(c => c.GetStatus()).Returns(FeatureConfigurationStatus.Undefined).Callback(() => + { + switch (++counter) + { + case 1: + before = DateTime.Now; + break; + case 2: + after = DateTime.Now; + sync.Set(); + break; + } + }); + + sut.Observe(configuration.Object, FeatureConfigurationStatus.Disabled); + sut.Start(); + sync.WaitOne(); + sut.Reset(); + + Assert.IsTrue(after - before >= new TimeSpan(0, 0, 0, 0, TIMEOUT)); + } + + [TestMethod] + public void MustStopWhenReset() + { + var configuration = new Mock(); + var counter = 0; + + configuration.Setup(c => c.GetStatus()).Returns(FeatureConfigurationStatus.Disabled).Callback(() => counter++); + + sut.Observe(configuration.Object, FeatureConfigurationStatus.Enabled); + sut.Start(); + Thread.Sleep(10); + sut.Reset(); + Thread.Sleep(10); + + configuration.Verify(c => c.GetStatus(), Times.Exactly(counter)); + } + + [TestMethod] + public void MustRemoveConfigurationsWhenReset() + { + var configuration = new Mock(); + var counter = 0; + + configuration.Setup(c => c.GetStatus()).Returns(FeatureConfigurationStatus.Disabled).Callback(() => counter++); + + sut.Observe(configuration.Object, FeatureConfigurationStatus.Enabled); + sut.Start(); + Thread.Sleep(10); + sut.Reset(); + + configuration.Verify(c => c.GetStatus(), Times.Exactly(counter)); + configuration.Reset(); + + sut.Start(); + Thread.Sleep(10); + sut.Reset(); + + configuration.Verify(c => c.GetStatus(), Times.Never); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void MustValidateTimeout() + { + new FeatureConfigurationMonitor(logger.Object, new Random().Next(int.MinValue, -1)); + } + } +} diff --git a/SafeExamBrowser.Lockdown.UnitTests/FeatureConfigurationStub.cs b/SafeExamBrowser.Lockdown.UnitTests/FeatureConfigurationStub.cs index b63ebdc3..9339a815 100644 --- a/SafeExamBrowser.Lockdown.UnitTests/FeatureConfigurationStub.cs +++ b/SafeExamBrowser.Lockdown.UnitTests/FeatureConfigurationStub.cs @@ -32,12 +32,12 @@ namespace SafeExamBrowser.Lockdown.UnitTests throw new NotImplementedException(); } - public void Initialize() + public FeatureConfigurationStatus GetStatus() { throw new NotImplementedException(); } - public void Monitor() + public void Initialize() { throw new NotImplementedException(); } diff --git a/SafeExamBrowser.Lockdown.UnitTests/SafeExamBrowser.Lockdown.UnitTests.csproj b/SafeExamBrowser.Lockdown.UnitTests/SafeExamBrowser.Lockdown.UnitTests.csproj index 4f7a1751..86494b2d 100644 --- a/SafeExamBrowser.Lockdown.UnitTests/SafeExamBrowser.Lockdown.UnitTests.csproj +++ b/SafeExamBrowser.Lockdown.UnitTests/SafeExamBrowser.Lockdown.UnitTests.csproj @@ -81,6 +81,7 @@ + diff --git a/SafeExamBrowser.Lockdown/AutoRestoreMechanism.cs b/SafeExamBrowser.Lockdown/AutoRestoreMechanism.cs index 182f70ec..fa0f4012 100644 --- a/SafeExamBrowser.Lockdown/AutoRestoreMechanism.cs +++ b/SafeExamBrowser.Lockdown/AutoRestoreMechanism.cs @@ -7,6 +7,7 @@ */ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using SafeExamBrowser.Contracts.Lockdown; @@ -48,7 +49,7 @@ namespace SafeExamBrowser.Lockdown if (!running) { running = true; - Task.Run(new Action(RestoreAll)); + Task.Run(new Action(AutoRestore)); logger.Info("Started auto-restore mechanism."); } else @@ -74,47 +75,31 @@ namespace SafeExamBrowser.Lockdown } } - private void RestoreAll() + private void AutoRestore() { + if (IsStopped()) + { + return; + } + var configurations = backup.GetAllConfigurations(); - var all = configurations.Count; - var restored = 0; if (configurations.Any()) { - logger.Info($"Attempting to restore {configurations.Count} items..."); + var success = TryRestoreAll(configurations); - foreach (var configuration in configurations) - { - var success = configuration.Restore(); - - if (success) - { - backup.Delete(configuration); - restored++; - } - else - { - logger.Warn($"Failed to restore {configuration}!"); - } - - lock (@lock) - { - if (!running) - { - logger.Info("Auto-restore mechanism was aborted."); - - return; - } - } - } - - if (all == restored) + if (success == true) { systemConfigurationUpdate.ExecuteAsync(); } - - Task.Delay(timeout_ms).ContinueWith((_) => RestoreAll()); + else if (success == false) + { + Task.Delay(timeout_ms).ContinueWith((_) => AutoRestore()); + } + else + { + logger.Info("Auto-restore mechanism was aborted."); + } } else { @@ -125,5 +110,55 @@ namespace SafeExamBrowser.Lockdown } } } + + private bool IsStopped() + { + lock (@lock) + { + return !running; + } + } + + private bool? TryRestoreAll(IList configurations) + { + var restored = 0; + + logger.Info($"Attempting to restore {configurations.Count} items..."); + + foreach (var configuration in configurations) + { + var success = TryRestore(configuration); + + if (success) + { + restored++; + } + + if (IsStopped()) + { + return null; + } + } + + logger.Info($"Restored {restored} of {configurations.Count} items."); + + return restored == configurations.Count; + } + + private bool TryRestore(IFeatureConfiguration configuration) + { + var success = configuration.Restore(); + + if (success) + { + backup.Delete(configuration); + } + else + { + logger.Warn($"Failed to restore {configuration}!"); + } + + return success; + } } } diff --git a/SafeExamBrowser.Lockdown/FeatureConfigurationMonitor.cs b/SafeExamBrowser.Lockdown/FeatureConfigurationMonitor.cs new file mode 100644 index 00000000..2d32c15b --- /dev/null +++ b/SafeExamBrowser.Lockdown/FeatureConfigurationMonitor.cs @@ -0,0 +1,148 @@ +/* + * 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.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using SafeExamBrowser.Contracts.Lockdown; +using SafeExamBrowser.Contracts.Logging; + +namespace SafeExamBrowser.Lockdown +{ + public class FeatureConfigurationMonitor : IFeatureConfigurationMonitor + { + private readonly object @lock = new object(); + + private IDictionary configurations; + private ILogger logger; + private readonly int timeout_ms; + private bool running; + + public FeatureConfigurationMonitor(ILogger logger, int timeout_ms) + { + if (timeout_ms < 0) + { + throw new ArgumentException("Must be 0 or greater!", nameof(timeout_ms)); + } + + this.configurations = new Dictionary(); + this.logger = logger; + this.timeout_ms = timeout_ms; + } + + public void Observe(IFeatureConfiguration configuration, FeatureConfigurationStatus status) + { + lock (@lock) + { + configurations.Add(configuration, status); + } + } + + public void Reset() + { + lock (@lock) + { + if (running) + { + running = false; + configurations.Clear(); + logger.Info("Stopped monitoring."); + } + else + { + logger.Info("Monitoring is not running."); + } + } + } + + public void Start() + { + lock (@lock) + { + if (!running) + { + running = true; + Task.Run(new Action(Monitor)); + logger.Info("Started monitoring."); + } + else + { + logger.Info("Monitoring is already running."); + } + } + } + + private void Monitor() + { + if (IsStopped()) + { + return; + } + + var configurations = new Dictionary(this.configurations); + + logger.Debug($"Checking {configurations.Count} configurations..."); + + foreach (var item in configurations) + { + var configuration = item.Key; + var status = item.Value; + + Enforce(configuration, status); + + if (IsStopped()) + { + logger.Info("Monitoring was aborted."); + + return; + } + } + + if (configurations.Any()) + { + Task.Delay(timeout_ms).ContinueWith((_) => Monitor()); + } + else + { + lock (@lock) + { + running = false; + logger.Info("Nothing to be monitored, stopped monitoring."); + } + } + } + + private void Enforce(IFeatureConfiguration configuration, FeatureConfigurationStatus status) + { + var currentStatus = configuration.GetStatus(); + + if (currentStatus != status) + { + logger.Warn($"{configuration} is {currentStatus.ToString().ToLower()} instead of {status.ToString().ToLower()}!"); + + if (status == FeatureConfigurationStatus.Disabled) + { + configuration.DisableFeature(); + } + else if (status == FeatureConfigurationStatus.Enabled) + { + configuration.EnableFeature(); + } + } + } + + private bool IsStopped() + { + lock (@lock) + { + return !running; + } + } + } +} diff --git a/SafeExamBrowser.Lockdown/FeatureConfigurations/FeatureConfiguration.cs b/SafeExamBrowser.Lockdown/FeatureConfigurations/FeatureConfiguration.cs index 5272c155..c2b2ab3a 100644 --- a/SafeExamBrowser.Lockdown/FeatureConfigurations/FeatureConfiguration.cs +++ b/SafeExamBrowser.Lockdown/FeatureConfigurations/FeatureConfiguration.cs @@ -31,8 +31,8 @@ namespace SafeExamBrowser.Lockdown.FeatureConfigurations public abstract bool DisableFeature(); public abstract bool EnableFeature(); + public abstract FeatureConfigurationStatus GetStatus(); public abstract void Initialize(); - public abstract void Monitor(); public abstract bool Restore(); public override string ToString() diff --git a/SafeExamBrowser.Lockdown/FeatureConfigurations/RegistryConfigurations/RegistryConfiguration.cs b/SafeExamBrowser.Lockdown/FeatureConfigurations/RegistryConfigurations/RegistryConfiguration.cs index 6c23dfcb..bb5f4d65 100644 --- a/SafeExamBrowser.Lockdown/FeatureConfigurations/RegistryConfigurations/RegistryConfiguration.cs +++ b/SafeExamBrowser.Lockdown/FeatureConfigurations/RegistryConfigurations/RegistryConfiguration.cs @@ -10,6 +10,7 @@ using System; using System.Collections.Generic; using System.Linq; using Microsoft.Win32; +using SafeExamBrowser.Contracts.Lockdown; using SafeExamBrowser.Contracts.Logging; namespace SafeExamBrowser.Lockdown.FeatureConfigurations.RegistryConfigurations @@ -88,9 +89,31 @@ namespace SafeExamBrowser.Lockdown.FeatureConfigurations.RegistryConfigurations } } - public override void Monitor() + public override FeatureConfigurationStatus GetStatus() { - // TODO! + var status = FeatureConfigurationStatus.Undefined; + + foreach (var item in Items) + { + var current = ReadItem(item.Key, item.Value); + + if (current.Data?.Equals(item.Disabled) == true && status != FeatureConfigurationStatus.Enabled) + { + status = FeatureConfigurationStatus.Disabled; + } + else if (current.Data?.Equals(item.Enabled) == true && status != FeatureConfigurationStatus.Disabled) + { + status = FeatureConfigurationStatus.Enabled; + } + else + { + status = FeatureConfigurationStatus.Undefined; + + break; + } + } + + return status; } public override bool Restore() diff --git a/SafeExamBrowser.Lockdown/FeatureConfigurations/WindowsUpdateConfiguration.cs b/SafeExamBrowser.Lockdown/FeatureConfigurations/WindowsUpdateConfiguration.cs index d4d0d6ca..56bbda17 100644 --- a/SafeExamBrowser.Lockdown/FeatureConfigurations/WindowsUpdateConfiguration.cs +++ b/SafeExamBrowser.Lockdown/FeatureConfigurations/WindowsUpdateConfiguration.cs @@ -7,6 +7,7 @@ */ using System; +using SafeExamBrowser.Contracts.Lockdown; using SafeExamBrowser.Contracts.Logging; namespace SafeExamBrowser.Lockdown.FeatureConfigurations @@ -28,12 +29,12 @@ namespace SafeExamBrowser.Lockdown.FeatureConfigurations return true; } - public override void Initialize() + public override FeatureConfigurationStatus GetStatus() { - + return FeatureConfigurationStatus.Undefined; } - public override void Monitor() + public override void Initialize() { } diff --git a/SafeExamBrowser.Lockdown/SafeExamBrowser.Lockdown.csproj b/SafeExamBrowser.Lockdown/SafeExamBrowser.Lockdown.csproj index 8ce3b927..2aa3e54c 100644 --- a/SafeExamBrowser.Lockdown/SafeExamBrowser.Lockdown.csproj +++ b/SafeExamBrowser.Lockdown/SafeExamBrowser.Lockdown.csproj @@ -56,6 +56,7 @@ + diff --git a/SafeExamBrowser.Service.UnitTests/Operations/LockdownOperationTests.cs b/SafeExamBrowser.Service.UnitTests/Operations/LockdownOperationTests.cs index aed98b29..7e419612 100644 --- a/SafeExamBrowser.Service.UnitTests/Operations/LockdownOperationTests.cs +++ b/SafeExamBrowser.Service.UnitTests/Operations/LockdownOperationTests.cs @@ -25,6 +25,7 @@ namespace SafeExamBrowser.Service.UnitTests.Operations { private Mock backup; private Mock factory; + private Mock monitor; private Mock logger; private Settings settings; private SessionContext sessionContext; @@ -35,6 +36,7 @@ namespace SafeExamBrowser.Service.UnitTests.Operations { backup = new Mock(); factory = new Mock(); + monitor = new Mock(); logger = new Mock(); settings = new Settings(); sessionContext = new SessionContext @@ -42,7 +44,7 @@ namespace SafeExamBrowser.Service.UnitTests.Operations Configuration = new ServiceConfiguration { Settings = settings, UserName = "TestName", UserSid = "S-1-234-TEST" } }; - sut = new LockdownOperation(backup.Object, factory.Object, logger.Object, sessionContext); + sut = new LockdownOperation(backup.Object, factory.Object, monitor.Object, logger.Object, sessionContext); } [TestMethod] @@ -63,7 +65,9 @@ namespace SafeExamBrowser.Service.UnitTests.Operations configuration.Verify(c => c.Initialize(), Times.Exactly(count)); configuration.Verify(c => c.DisableFeature(), Times.Exactly(3)); configuration.Verify(c => c.EnableFeature(), Times.Exactly(count - 3)); - configuration.Verify(c => c.Monitor(), Times.Exactly(count)); + monitor.Verify(m => m.Observe(It.Is(c => c == configuration.Object), It.Is(s => s == FeatureConfigurationStatus.Disabled)), Times.Exactly(3)); + monitor.Verify(m => m.Observe(It.Is(c => c == configuration.Object), It.Is(s => s == FeatureConfigurationStatus.Enabled)), Times.Exactly(count - 3)); + monitor.Verify(m => m.Start(), Times.Once); Assert.AreEqual(OperationResult.Success, result); } @@ -114,7 +118,8 @@ namespace SafeExamBrowser.Service.UnitTests.Operations configuration.Verify(c => c.Initialize(), Times.Exactly(count - offset)); configuration.Verify(c => c.DisableFeature(), Times.Never); configuration.Verify(c => c.EnableFeature(), Times.Exactly(count - offset)); - configuration.Verify(c => c.Monitor(), Times.Exactly(count - offset - 1)); + monitor.Verify(m => m.Observe(It.Is(c => c == configuration.Object), It.Is(s => s == FeatureConfigurationStatus.Enabled)), Times.Exactly(count - offset - 1)); + monitor.Verify(m => m.Start(), Times.Never); Assert.AreEqual(OperationResult.Failed, result); } @@ -167,6 +172,8 @@ namespace SafeExamBrowser.Service.UnitTests.Operations configuration4.Verify(c => c.Initialize(), Times.Never); configuration4.Verify(c => c.Restore(), Times.Once); + monitor.Verify(m => m.Reset(), Times.Once); + Assert.AreEqual(OperationResult.Success, result); } @@ -218,6 +225,8 @@ namespace SafeExamBrowser.Service.UnitTests.Operations configuration4.Verify(c => c.Initialize(), Times.Never); configuration4.Verify(c => c.Restore(), Times.Once); + monitor.Verify(m => m.Reset(), Times.Once); + Assert.AreEqual(OperationResult.Failed, result); } } diff --git a/SafeExamBrowser.Service/CompositionRoot.cs b/SafeExamBrowser.Service/CompositionRoot.cs index 86e8798e..2ccda4c8 100644 --- a/SafeExamBrowser.Service/CompositionRoot.cs +++ b/SafeExamBrowser.Service/CompositionRoot.cs @@ -35,6 +35,7 @@ namespace SafeExamBrowser.Service internal void BuildObjectGraph() { + const int ONE_SECOND = 1000; const int FIVE_SECONDS = 5000; var backupFilePath = BuildBackupFilePath(); @@ -43,6 +44,7 @@ namespace SafeExamBrowser.Service var featureBackup = new FeatureConfigurationBackup(backupFilePath, new ModuleLogger(logger, nameof(FeatureConfigurationBackup))); var featureFactory = new FeatureConfigurationFactory(new ModuleLogger(logger, nameof(FeatureConfigurationFactory))); + var featureMonitor = new FeatureConfigurationMonitor(new ModuleLogger(logger, nameof(FeatureConfigurationMonitor)), ONE_SECOND); 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(); @@ -58,7 +60,7 @@ namespace SafeExamBrowser.Service bootstrapOperations.Enqueue(new ServiceEventCleanupOperation(logger, sessionContext)); sessionOperations.Enqueue(new SessionInitializationOperation(logger, ServiceEventFactory, sessionContext)); - sessionOperations.Enqueue(new LockdownOperation(featureBackup, featureFactory, logger, sessionContext)); + sessionOperations.Enqueue(new LockdownOperation(featureBackup, featureFactory, featureMonitor, logger, sessionContext)); sessionOperations.Enqueue(new SessionActivationOperation(logger, sessionContext)); var bootstrapSequence = new OperationSequence(logger, bootstrapOperations); diff --git a/SafeExamBrowser.Service/Operations/LockdownOperation.cs b/SafeExamBrowser.Service/Operations/LockdownOperation.cs index 382a8750..15fce865 100644 --- a/SafeExamBrowser.Service/Operations/LockdownOperation.cs +++ b/SafeExamBrowser.Service/Operations/LockdownOperation.cs @@ -17,17 +17,20 @@ namespace SafeExamBrowser.Service.Operations { private IFeatureConfigurationBackup backup; private IFeatureConfigurationFactory factory; + private IFeatureConfigurationMonitor monitor; private ILogger logger; private Guid groupId; public LockdownOperation( IFeatureConfigurationBackup backup, IFeatureConfigurationFactory factory, + IFeatureConfigurationMonitor monitor, ILogger logger, SessionContext sessionContext) : base(sessionContext) { this.backup = backup; this.factory = factory; + this.monitor = monitor; this.logger = logger; } @@ -58,7 +61,7 @@ namespace SafeExamBrowser.Service.Operations foreach (var (configuration, disable) in configurations) { - success &= SetConfiguration(configuration, disable); + success &= TrySet(configuration, disable); if (!success) { @@ -68,6 +71,7 @@ namespace SafeExamBrowser.Service.Operations if (success) { + monitor.Start(); logger.Info("Lockdown successful."); } else @@ -85,19 +89,11 @@ namespace SafeExamBrowser.Service.Operations var configurations = backup.GetBy(groupId); var success = true; + monitor.Reset(); + foreach (var configuration in configurations) { - var restored = configuration.Restore(); - - if (restored) - { - backup.Delete(configuration); - } - else - { - logger.Error($"Failed to restore {configuration}!"); - success = false; - } + success &= TryRestore(configuration); } if (success) @@ -112,9 +108,26 @@ namespace SafeExamBrowser.Service.Operations return success ? OperationResult.Success : OperationResult.Failed; } - private bool SetConfiguration(IFeatureConfiguration configuration, bool disable) + private bool TryRestore(IFeatureConfiguration configuration) + { + var success = configuration.Restore(); + + if (success) + { + backup.Delete(configuration); + } + else + { + logger.Error($"Failed to restore {configuration}!"); + } + + return success; + } + + private bool TrySet(IFeatureConfiguration configuration, bool disable) { var success = false; + var status = FeatureConfigurationStatus.Undefined; configuration.Initialize(); backup.Save(configuration); @@ -122,15 +135,17 @@ namespace SafeExamBrowser.Service.Operations if (disable) { success = configuration.DisableFeature(); + status = FeatureConfigurationStatus.Disabled; } else { success = configuration.EnableFeature(); + status = FeatureConfigurationStatus.Enabled; } if (success) { - configuration.Monitor(); + monitor.Observe(configuration, status); } else {