SEBWIN-319: Implemented system configuration update for service operation and auto-restore mechanism.

This commit is contained in:
dbuechel 2019-07-03 12:27:02 +02:00
parent 2f510096d0
commit 39b63218fb
13 changed files with 182 additions and 43 deletions

View file

@ -91,6 +91,7 @@ namespace SafeExamBrowser.Contracts.I18n
OperationStatus_StopWindowMonitoring,
OperationStatus_TerminateBrowser,
OperationStatus_TerminateShell,
OperationStatus_UpdateSystemConfiguration,
OperationStatus_WaitExplorerStartup,
OperationStatus_WaitExplorerTermination,
OperationStatus_WaitRuntimeDisconnection,

View file

@ -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
{
/// <summary>
/// Provides functionality to update and enforce the system configuration.
/// </summary>
public interface ISystemConfigurationUpdate
{
/// <summary>
/// Executes the update synchronously.
/// </summary>
void Execute();
/// <summary>
/// Executes the udpate asynchronously.
/// </summary>
void ExecuteAsync();
}
}

View file

@ -96,6 +96,7 @@
<Compile Include="Lockdown\IFeatureConfiguration.cs" />
<Compile Include="Lockdown\IFeatureConfigurationBackup.cs" />
<Compile Include="Lockdown\IFeatureConfigurationFactory.cs" />
<Compile Include="Lockdown\ISystemConfigurationUpdate.cs" />
<Compile Include="Runtime\IRuntimeController.cs" />
<Compile Include="Core\OperationModel\Events\ActionRequiredEventArgs.cs" />
<Compile Include="Core\OperationModel\Events\ActionRequiredEventHandler.cs" />

View file

@ -231,6 +231,9 @@
<Entry key="OperationStatus_TerminateShell">
Terminating user interface
</Entry>
<Entry key="OperationStatus_UpdateSystemConfiguration">
Updating system configuration
</Entry>
<Entry key="OperationStatus_WaitExplorerStartup">
Waiting for Windows explorer to start up
</Entry>

View file

@ -21,6 +21,7 @@ namespace SafeExamBrowser.Lockdown.UnitTests
{
private Mock<IFeatureConfigurationBackup> backup;
private Mock<ILogger> logger;
private Mock<ISystemConfigurationUpdate> systemConfigurationUpdate;
private AutoRestoreMechanism sut;
[TestInitialize]
@ -28,8 +29,9 @@ namespace SafeExamBrowser.Lockdown.UnitTests
{
backup = new Mock<IFeatureConfigurationBackup>();
logger = new Mock<ILogger>();
systemConfigurationUpdate = new Mock<ISystemConfigurationUpdate>();
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<IFeatureConfiguration> { configuration.Object };
var sync = new AutoResetEvent(false);
backup.Setup(b => b.GetAllConfigurations()).Returns(list).Callback(() => counter++);
backup.Setup(b => b.Delete(It.IsAny<IFeatureConfiguration>())).Callback(() => { list.Clear(); sync.Set(); });
configuration.Setup(c => c.Restore()).Returns(() => counter == limit);
backup.Setup(b => b.GetAllConfigurations()).Returns(() => new List<IFeatureConfiguration>(list)).Callback(() => counter++);
backup.Setup(b => b.Delete(It.IsAny<IFeatureConfiguration>())).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<IFeatureConfiguration>(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<IFeatureConfiguration> { 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<ArgumentException>(() => 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));
}
}
}

View file

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

View file

@ -75,6 +75,7 @@
<Compile Include="FeatureConfigurations\RegistryConfigurations\UserHive\VmwareOverlayConfiguration.cs" />
<Compile Include="FeatureConfigurations\WindowsUpdateConfiguration.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SystemConfigurationUpdate.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SafeExamBrowser.Contracts\SafeExamBrowser.Contracts.csproj">

View file

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

View file

@ -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<ISystemConfigurationUpdate> systemConfigurationUpdate;
private Mock<IUserInfo> 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<ISystemConfigurationUpdate>();
userInfo = new Mock<IUserInfo>();
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<ServiceConfiguration>()), 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<ServiceConfiguration>())).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<ServiceConfiguration>()), 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<Guid>())).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<Guid>()), Times.Once);
service.Verify(s => s.StartSession(It.IsAny<ServiceConfiguration>()), 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<Guid>())).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<Guid>())).Returns(new CommunicationResult(true)).Callback(() => serviceEvent.Set());
PerformNormally();
systemConfigurationUpdate.Reset();
var result = sut.Revert();
service.Verify(s => s.StopSession(It.IsAny<Guid>()), 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<Guid>())).Returns(new CommunicationResult(false));
PerformNormally();
systemConfigurationUpdate.Reset();
var result = sut.Revert();
service.Verify(s => s.StopSession(It.IsAny<Guid>()), 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<Guid>())).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();

View file

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

View file

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

View file

@ -155,6 +155,10 @@
<Project>{10c62628-8e6a-45aa-9d97-339b119ad21d}</Project>
<Name>SafeExamBrowser.I18n</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.Lockdown\SafeExamBrowser.Lockdown.csproj">
<Project>{386b6042-3e12-4753-9fc6-c88ea4f97030}</Project>
<Name>SafeExamBrowser.Lockdown</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.Logging\SafeExamBrowser.Logging.csproj">
<Project>{e107026c-2011-4552-a7d8-3a0d37881df6}</Project>
<Name>SafeExamBrowser.Logging</Name>

View file

@ -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<IOperation>();
var sessionOperations = new Queue<IOperation>();
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));