SEBWIN-219: Implemented basic service operation.

This commit is contained in:
dbuechel 2018-01-24 12:34:32 +01:00
parent 7d5c6a1b0b
commit ebc12934bf
22 changed files with 389 additions and 36 deletions

View file

@ -15,6 +15,7 @@ namespace SafeExamBrowser.Configuration.Settings
internal class Settings : ISettings
{
public ConfigurationMode ConfigurationMode { get; set; }
public ServicePolicy ServicePolicy { get; set; }
public IBrowserSettings Browser { get; set; }
public IKeyboardSettings Keyboard { get; set; }

View file

@ -0,0 +1,17 @@
/*
* Copyright (c) 2018 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;
namespace SafeExamBrowser.Contracts.Communication
{
public interface IClientProxy
{
bool Connect(Guid token);
}
}

View file

@ -14,7 +14,7 @@ using SafeExamBrowser.Contracts.Communication.Responses;
namespace SafeExamBrowser.Contracts.Communication
{
[ServiceContract(SessionMode = SessionMode.Required)]
public interface ICommunicationHost
public interface ICommunication
{
/// <summary>
/// Initiates a connection to the host and must thus be called before any other opertion. To authenticate itself to the host, the

View file

@ -0,0 +1,17 @@
/*
* Copyright (c) 2018 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;
namespace SafeExamBrowser.Contracts.Communication
{
public interface IRuntimeProxy
{
bool Connect(Guid token);
}
}

View file

@ -0,0 +1,23 @@
/*
* Copyright (c) 2018 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.Communication
{
public interface IServiceProxy
{
/// <summary>
/// Tries to connect to the service host.
/// </summary>
bool Connect();
/// <summary>
/// Disconnects from the service host.
/// </summary>
void Disconnect();
}
}

View file

@ -7,11 +7,10 @@
*/
using System;
using System.Runtime.Serialization;
namespace SafeExamBrowser.Contracts.Communication.Messages
{
public interface IMessage : ISerializable
public interface IMessage
{
/// <summary>
/// The communication token needed for authentication with the host.

View file

@ -6,11 +6,9 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System.Runtime.Serialization;
namespace SafeExamBrowser.Contracts.Communication.Responses
{
public interface IResponse : ISerializable
public interface IResponse
{
}
}

View file

@ -10,7 +10,16 @@ namespace SafeExamBrowser.Contracts.Configuration.Settings
{
public enum ConfigurationMode
{
/// <summary>
/// In this mode, the application settings shall be used to configure the local client settings of a user. When running in this
/// mode, the user has the possiblity to re-configure the application during runtime.
/// </summary>
ConfigureClient,
/// <summary>
/// In this mode, the application settings shall only be used to start an exam. When running in this mode, the user cannot re-
/// configure the application during runtime.
/// </summary>
Exam
}
}

View file

@ -30,6 +30,11 @@ namespace SafeExamBrowser.Contracts.Configuration.Settings
/// </summary>
IMouseSettings Mouse { get; }
/// <summary>
/// The active service policy.
/// </summary>
ServicePolicy ServicePolicy { get; }
/// <summary>
/// All taskbar-related settings.
/// </summary>

View file

@ -0,0 +1,23 @@
/*
* Copyright (c) 2018 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.Configuration.Settings
{
public enum ServicePolicy
{
/// <summary>
/// The service component must be running. If it is not running, the user won't be able to start the application.
/// </summary>
Mandatory,
/// <summary>
/// The service component is optional. If it is not running, all service-related actions are simply skipped.
/// </summary>
Optional
}
}

View file

@ -25,10 +25,12 @@ namespace SafeExamBrowser.Contracts.I18n
MessageBox_StartupErrorTitle,
Notification_AboutTooltip,
Notification_LogTooltip,
SplashScreen_CloseServiceConnection,
SplashScreen_EmptyClipboard,
SplashScreen_InitializeBrowser,
SplashScreen_InitializeConfiguration,
SplashScreen_InitializeProcessMonitoring,
SplashScreen_InitializeServiceConnection,
SplashScreen_InitializeTaskbar,
SplashScreen_InitializeWindowMonitoring,
SplashScreen_InitializeWorkingArea,

View file

@ -55,7 +55,10 @@
<ItemGroup>
<Compile Include="Behaviour\IApplicationController.cs" />
<Compile Include="Behaviour\IRuntimeController.cs" />
<Compile Include="Communication\ICommunicationHost.cs" />
<Compile Include="Communication\ICommunication.cs" />
<Compile Include="Communication\IClientProxy.cs" />
<Compile Include="Communication\IRuntimeProxy.cs" />
<Compile Include="Communication\IServiceProxy.cs" />
<Compile Include="Communication\Messages\IMessage.cs" />
<Compile Include="Communication\Responses\IResponse.cs" />
<Compile Include="Communication\Responses\IConnectResponse.cs" />
@ -78,6 +81,7 @@
<Compile Include="Behaviour\IStartupController.cs" />
<Compile Include="Configuration\Settings\ISettingsRepository.cs" />
<Compile Include="Configuration\Settings\ITaskbarSettings.cs" />
<Compile Include="Configuration\Settings\ServicePolicy.cs" />
<Compile Include="I18n\IText.cs" />
<Compile Include="I18n\TextKey.cs" />
<Compile Include="Logging\ILogContent.cs" />

View file

@ -15,30 +15,33 @@ using SafeExamBrowser.Contracts.Logging;
namespace SafeExamBrowser.Core.Communication
{
public class CommunicationHostProxy : ICommunicationHost
public abstract class BaseProxy : ICommunication
{
private string address;
private ILogger logger;
private ICommunicationHost channel;
private ICommunication channel;
public CommunicationHostProxy(ILogger logger, string address)
protected Guid? CommunicationToken { get; private set; }
protected ILogger Logger { get; private set; }
public BaseProxy(ILogger logger, string address)
{
this.address = address;
this.logger = logger;
this.Logger = logger;
}
public IConnectResponse Connect(Guid? token = null)
{
var endpoint = new EndpointAddress(address);
channel = ChannelFactory<ICommunicationHost>.CreateChannel(new NetNamedPipeBinding(NetNamedPipeSecurityMode.Transport), endpoint);
channel = ChannelFactory<ICommunication>.CreateChannel(new NetNamedPipeBinding(NetNamedPipeSecurityMode.Transport), endpoint);
(channel as ICommunicationObject).Closed += CommunicationHostProxy_Closed;
(channel as ICommunicationObject).Closing += CommunicationHostProxy_Closing;
(channel as ICommunicationObject).Faulted += CommunicationHostProxy_Faulted;
var response = channel.Connect(token);
logger.Debug($"Tried to connect to {address}, connection was {(response.ConnectionEstablished ? "established" : "refused")}.");
CommunicationToken = response.CommunicationToken;
Logger.Debug($"Tried to connect to {address}, connection was {(response.ConnectionEstablished ? "established" : "refused")}.");
return response;
}
@ -48,7 +51,7 @@ namespace SafeExamBrowser.Core.Communication
if (ChannelIsReady())
{
channel.Disconnect(message);
logger.Debug($"Disconnected from {address}, transmitting {ToString(message)}.");
Logger.Debug($"Disconnected from {address}, transmitting {ToString(message)}.");
}
throw new CommunicationException($"Tried to disconnect from host, but channel was {GetChannelState()}!");
@ -60,7 +63,7 @@ namespace SafeExamBrowser.Core.Communication
{
var response = channel.Send(message);
logger.Debug($"Sent {ToString(message)}, got {ToString(response)}.");
Logger.Debug($"Sent {ToString(message)}, got {ToString(response)}.");
return response;
}
@ -68,6 +71,14 @@ namespace SafeExamBrowser.Core.Communication
throw new CommunicationException($"Tried to send {ToString(message)}, but channel was {GetChannelState()}!");
}
protected void FailIfNotConnected(string operationName)
{
if (!CommunicationToken.HasValue)
{
throw new InvalidOperationException($"Cannot perform '{operationName}' before being connected to endpoint!");
}
}
private bool ChannelIsReady()
{
return channel != null && (channel as ICommunicationObject).State == CommunicationState.Opened;
@ -75,17 +86,17 @@ namespace SafeExamBrowser.Core.Communication
private void CommunicationHostProxy_Closed(object sender, EventArgs e)
{
logger.Debug("Communication channel has been closed.");
Logger.Debug("Communication channel has been closed.");
}
private void CommunicationHostProxy_Closing(object sender, EventArgs e)
{
logger.Debug("Communication channel is closing.");
Logger.Debug("Communication channel is closing.");
}
private void CommunicationHostProxy_Faulted(object sender, EventArgs e)
{
logger.Error("Communication channel has faulted!");
Logger.Error("Communication channel has faulted!");
}
private string GetChannelState()

View file

@ -0,0 +1,19 @@
/*
* Copyright (c) 2018 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 SafeExamBrowser.Contracts.Communication.Messages;
namespace SafeExamBrowser.Core.Communication.Messages
{
[Serializable]
internal class Message : IMessage
{
public Guid CommunicationToken { get; set; }
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2018 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 SafeExamBrowser.Contracts.Communication;
using SafeExamBrowser.Contracts.Logging;
using SafeExamBrowser.Core.Communication.Messages;
namespace SafeExamBrowser.Core.Communication
{
public class ServiceProxy : BaseProxy, IServiceProxy
{
public ServiceProxy(ILogger logger, string address) : base(logger, address)
{
}
public bool Connect()
{
throw new NotImplementedException();
}
public void Disconnect()
{
FailIfNotConnected(nameof(Disconnect));
Disconnect(new Message { CommunicationToken = CommunicationToken.Value });
}
}
}

View file

@ -30,6 +30,9 @@
<Entry key="Notification_LogTooltip">
Application Log
</Entry>
<Entry key="SplashScreen_CloseServiceConnection">
Closing service connection
</Entry>
<Entry key="SplashScreen_EmptyClipboard">
Emptying clipboard
</Entry>
@ -42,6 +45,9 @@
<Entry key="SplashScreen_InitializeProcessMonitoring">
Initializing process monitoring
</Entry>
<Entry key="SplashScreen_InitializeServiceConnection">
Initializing service connection
</Entry>
<Entry key="SplashScreen_InitializeTaskbar">
Initializing taskbar
</Entry>

View file

@ -58,7 +58,9 @@
<Compile Include="Behaviour\Operations\I18nOperation.cs" />
<Compile Include="Behaviour\ShutdownController.cs" />
<Compile Include="Behaviour\StartupController.cs" />
<Compile Include="Communication\CommunicationHostProxy.cs" />
<Compile Include="Communication\BaseProxy.cs" />
<Compile Include="Communication\Messages\Message.cs" />
<Compile Include="Communication\ServiceProxy.cs" />
<Compile Include="Logging\DefaultLogFormatter.cs" />
<Compile Include="Logging\LogFileWriter.cs" />
<Compile Include="Logging\LogMessage.cs" />
@ -82,6 +84,8 @@
<Name>SafeExamBrowser.Contracts</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup />
<ItemGroup>
<Folder Include="Communication\Responses\" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View file

@ -6,17 +6,151 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using SafeExamBrowser.Contracts.Communication;
using SafeExamBrowser.Contracts.Configuration.Settings;
using SafeExamBrowser.Contracts.I18n;
using SafeExamBrowser.Contracts.Logging;
using SafeExamBrowser.Contracts.UserInterface;
using SafeExamBrowser.Runtime.Behaviour.Operations;
namespace SafeExamBrowser.Runtime.UnitTests.Behaviour.Operations
{
[TestClass]
public class ServiceOperationTests
{
[TestMethod]
public void Test()
private Mock<ILogger> logger;
private Mock<IServiceProxy> service;
private Mock<ISettingsRepository> settings;
private Mock<ISplashScreen> splashScreen;
private Mock<IText> text;
private ServiceOperation sut;
[TestInitialize]
public void Initialize()
{
Assert.Fail();
logger = new Mock<ILogger>();
service = new Mock<IServiceProxy>();
settings = new Mock<ISettingsRepository>();
splashScreen = new Mock<ISplashScreen>();
text = new Mock<IText>();
sut = new ServiceOperation(logger.Object, service.Object, settings.Object, text.Object)
{
SplashScreen = splashScreen.Object
};
}
[TestMethod]
public void MustConnectToService()
{
service.Setup(s => s.Connect()).Returns(true);
settings.SetupGet(s => s.Current.ServicePolicy).Returns(ServicePolicy.Mandatory);
sut.Perform();
service.Setup(s => s.Connect()).Returns(true);
settings.SetupGet(s => s.Current.ServicePolicy).Returns(ServicePolicy.Optional);
sut.Perform();
service.Verify(s => s.Connect(), Times.Exactly(2));
}
[TestMethod]
public void MustNotFailIfServiceNotAvailable()
{
service.Setup(s => s.Connect()).Returns(false);
settings.SetupGet(s => s.Current.ServicePolicy).Returns(ServicePolicy.Mandatory);
sut.Perform();
service.Setup(s => s.Connect()).Returns(false);
settings.SetupGet(s => s.Current.ServicePolicy).Returns(ServicePolicy.Optional);
sut.Perform();
service.Setup(s => s.Connect()).Throws<Exception>();
settings.SetupGet(s => s.Current.ServicePolicy).Returns(ServicePolicy.Mandatory);
sut.Perform();
service.Setup(s => s.Connect()).Throws<Exception>();
settings.SetupGet(s => s.Current.ServicePolicy).Returns(ServicePolicy.Optional);
sut.Perform();
}
[TestMethod]
public void MustAbortIfServiceMandatoryAndNotAvailable()
{
service.Setup(s => s.Connect()).Returns(false);
settings.SetupGet(s => s.Current.ServicePolicy).Returns(ServicePolicy.Mandatory);
sut.Perform();
Assert.IsTrue(sut.AbortStartup);
}
[TestMethod]
public void MustNotAbortIfServiceOptionalAndNotAvailable()
{
service.Setup(s => s.Connect()).Returns(false);
settings.SetupGet(s => s.Current.ServicePolicy).Returns(ServicePolicy.Optional);
sut.Perform();
Assert.IsFalse(sut.AbortStartup);
}
[TestMethod]
public void MustDisconnectWhenReverting()
{
service.Setup(s => s.Connect()).Returns(true);
settings.SetupGet(s => s.Current.ServicePolicy).Returns(ServicePolicy.Mandatory);
sut.Perform();
sut.Revert();
service.Setup(s => s.Connect()).Returns(true);
settings.SetupGet(s => s.Current.ServicePolicy).Returns(ServicePolicy.Optional);
sut.Perform();
sut.Revert();
service.Verify(s => s.Disconnect(), Times.Exactly(2));
}
[TestMethod]
public void MustNotDisconnnectIfNotAvailable()
{
service.Setup(s => s.Connect()).Returns(false);
settings.SetupGet(s => s.Current.ServicePolicy).Returns(ServicePolicy.Mandatory);
sut.Perform();
sut.Revert();
service.Setup(s => s.Connect()).Returns(false);
settings.SetupGet(s => s.Current.ServicePolicy).Returns(ServicePolicy.Optional);
sut.Perform();
sut.Revert();
service.Setup(s => s.Connect()).Throws<Exception>();
settings.SetupGet(s => s.Current.ServicePolicy).Returns(ServicePolicy.Mandatory);
sut.Perform();
sut.Revert();
service.Setup(s => s.Connect()).Throws<Exception>();
settings.SetupGet(s => s.Current.ServicePolicy).Returns(ServicePolicy.Optional);
sut.Perform();
sut.Revert();
service.Verify(s => s.Disconnect(), Times.Never);
}
}
}

View file

@ -14,7 +14,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Behaviour
public class RuntimeControllerTests
{
[TestMethod]
public void Test()
public void TODO()
{
Assert.Fail();
}

View file

@ -6,9 +6,11 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using SafeExamBrowser.Contracts.Behaviour;
using SafeExamBrowser.Contracts.Communication;
using SafeExamBrowser.Contracts.Configuration.Settings;
using SafeExamBrowser.Contracts.I18n;
using SafeExamBrowser.Contracts.Logging;
using SafeExamBrowser.Contracts.UserInterface;
@ -16,31 +18,76 @@ namespace SafeExamBrowser.Runtime.Behaviour.Operations
{
internal class ServiceOperation : IOperation
{
private ICommunicationHost serviceHost;
private bool serviceAvailable;
private bool serviceMandatory;
private ILogger logger;
private IServiceProxy service;
private ISettingsRepository settingsRepository;
private IText text;
public bool AbortStartup { get; private set; }
public ISplashScreen SplashScreen { private get; set; }
public ServiceOperation(ICommunicationHost serviceHost, ILogger logger, ISettingsRepository settingsRepository)
public ServiceOperation(ILogger logger, IServiceProxy service, ISettingsRepository settingsRepository, IText text)
{
this.serviceHost = serviceHost;
this.service = service;
this.logger = logger;
this.settingsRepository = settingsRepository;
this.text = text;
}
public void Perform()
{
logger.Info("Initializing service connection...");
// SplashScreen.UpdateText(...)
logger.Info($"Initializing service connection...");
SplashScreen.UpdateText(TextKey.SplashScreen_InitializeServiceConnection);
// TODO
try
{
serviceMandatory = settingsRepository.Current.ServicePolicy == ServicePolicy.Mandatory;
serviceAvailable = service.Connect();
}
catch (Exception e)
{
var message = "Failed to connect to the service component!";
if (serviceMandatory)
{
logger.Error(message, e);
}
else
{
logger.Info($"{message} Reason: {e.Message}");
}
}
AbortStartup = serviceMandatory && !serviceAvailable;
if (AbortStartup)
{
logger.Info("Aborting startup because the service is mandatory but not available!");
}
else
{
logger.Info($"The service is {(serviceMandatory ? "mandatory" : "optional")} and {(!serviceAvailable ? "not" : "")} available.");
}
}
public void Revert()
{
// TODO
logger.Info("Closing service connection...");
SplashScreen.UpdateText(TextKey.SplashScreen_CloseServiceConnection);
if (serviceAvailable)
{
try
{
service.Disconnect();
}
catch (Exception e)
{
logger.Error("Failed to disconnect from service component!", e);
}
}
}
}
}

View file

@ -17,7 +17,7 @@ namespace SafeExamBrowser.Runtime.Behaviour
{
internal class RuntimeController : IRuntimeController
{
private ICommunicationHost serviceProxy;
private ICommunication serviceProxy;
private Queue<IOperation> operations;
private ILogger logger;
private ISettingsRepository settingsRepository;
@ -27,7 +27,7 @@ namespace SafeExamBrowser.Runtime.Behaviour
public ISettings Settings { private get; set; }
public RuntimeController(
ICommunicationHost serviceProxy,
ICommunication serviceProxy,
ILogger logger,
ISettingsRepository settingsRepository,
IShutdownController shutdownController,

View file

@ -51,7 +51,7 @@ namespace SafeExamBrowser.Runtime
InitializeLogging();
var text = new Text(logger);
var serviceProxy = new CommunicationHostProxy(new ModuleLogger(logger, typeof(CommunicationHostProxy)), "net.pipe://localhost/safeexambrowser/service");
var serviceProxy = new ServiceProxy(new ModuleLogger(logger, typeof(ServiceProxy)), "net.pipe://localhost/safeexambrowser/service");
var shutdownController = new ShutdownController(logger, runtimeInfo, text, uiFactory);
var startupController = new StartupController(logger, runtimeInfo, systemInfo, text, uiFactory);
@ -60,7 +60,7 @@ namespace SafeExamBrowser.Runtime
StartupOperations = new Queue<IOperation>();
StartupOperations.Enqueue(new I18nOperation(logger, text));
StartupOperations.Enqueue(new ConfigurationOperation(logger, runtimeInfo, settingsRepository, text, uiFactory, args));
StartupOperations.Enqueue(new ServiceOperation(serviceProxy, logger, settingsRepository));
StartupOperations.Enqueue(new ServiceOperation(logger, serviceProxy, settingsRepository, text));
//StartupOperations.Enqueue(new KioskModeOperation());
}