SEBWIN-301: Implemented basic service session procedure.

This commit is contained in:
dbuechel 2019-06-18 10:18:56 +02:00
parent 77a3b50ca9
commit 6b24554abc
61 changed files with 911 additions and 296 deletions

View file

@ -105,7 +105,12 @@ namespace SafeExamBrowser.Client.UnitTests.Communication
sut.AuthenticationToken = token;
var connectionResponse = sut.Connect(token);
var response = sut.Disconnect(new DisconnectionMessage { CommunicationToken = connectionResponse.CommunicationToken.Value });
var message = new DisconnectionMessage
{
CommunicationToken = connectionResponse.CommunicationToken.Value,
Interlocutor = Interlocutor.Runtime
};
var response = sut.Disconnect(message);
Assert.IsNotNull(response);
Assert.IsTrue(response.ConnectionTerminated);
@ -118,8 +123,13 @@ namespace SafeExamBrowser.Client.UnitTests.Communication
{
var token = sut.AuthenticationToken = Guid.NewGuid();
var response = sut.Connect(token);
var message = new DisconnectionMessage
{
CommunicationToken = response.CommunicationToken.Value,
Interlocutor = Interlocutor.Runtime
};
sut.Disconnect(new DisconnectionMessage { CommunicationToken = response.CommunicationToken.Value });
sut.Disconnect(message);
sut.AuthenticationToken = token = Guid.NewGuid();
response = sut.Connect(token);
@ -274,7 +284,7 @@ namespace SafeExamBrowser.Client.UnitTests.Communication
sut.Send(new PasswordRequestMessage(default(PasswordRequestPurpose), Guid.Empty) { CommunicationToken = token });
sut.Send(new ReconfigurationDeniedMessage("") { CommunicationToken = token });
sut.Send(new SimpleMessage(SimpleMessagePurport.Shutdown) { CommunicationToken = token });
sut.Disconnect(new DisconnectionMessage { CommunicationToken = token });
sut.Disconnect(new DisconnectionMessage { CommunicationToken = token, Interlocutor = Interlocutor.Runtime });
}
private class TestMessage : Message { };

View file

@ -65,12 +65,14 @@ namespace SafeExamBrowser.Client.UnitTests.Operations
{
runtime.Setup(r => r.Connect(It.IsAny<Guid>(), It.IsAny<bool>())).Returns(true);
runtime.Setup(r => r.Disconnect()).Returns(true);
runtime.SetupGet(r => r.IsConnected).Returns(true);
sut.Perform();
var result = sut.Revert();
runtime.Verify(r => r.Connect(It.IsAny<Guid>(), It.IsAny<bool>()), Times.Once);
runtime.Verify(r => r.Disconnect(), Times.Once);
runtime.VerifyGet(r => r.IsConnected, Times.Once);
runtime.VerifyNoOtherCalls();
Assert.AreEqual(OperationResult.Success, result);
@ -81,12 +83,14 @@ namespace SafeExamBrowser.Client.UnitTests.Operations
{
runtime.Setup(r => r.Connect(It.IsAny<Guid>(), It.IsAny<bool>())).Returns(true);
runtime.Setup(r => r.Disconnect()).Returns(false);
runtime.SetupGet(r => r.IsConnected).Returns(true);
sut.Perform();
var result = sut.Revert();
runtime.Verify(r => r.Connect(It.IsAny<Guid>(), It.IsAny<bool>()), Times.Once);
runtime.Verify(r => r.Disconnect(), Times.Once);
runtime.VerifyGet(r => r.IsConnected, Times.Once);
runtime.VerifyNoOtherCalls();
Assert.AreEqual(OperationResult.Failed, result);
@ -97,6 +101,7 @@ namespace SafeExamBrowser.Client.UnitTests.Operations
{
var result = sut.Revert();
runtime.VerifyGet(r => r.IsConnected, Times.Once);
runtime.VerifyNoOtherCalls();
Assert.AreEqual(OperationResult.Success, result);

View file

@ -8,6 +8,7 @@
using System;
using SafeExamBrowser.Communication.Hosts;
using SafeExamBrowser.Contracts.Communication;
using SafeExamBrowser.Contracts.Communication.Data;
using SafeExamBrowser.Contracts.Communication.Events;
using SafeExamBrowser.Contracts.Communication.Hosts;
@ -53,10 +54,13 @@ namespace SafeExamBrowser.Client.Communication
return accepted;
}
protected override void OnDisconnect()
protected override void OnDisconnect(Interlocutor interlocutor)
{
RuntimeDisconnected?.Invoke();
IsConnected = false;
if (interlocutor == Interlocutor.Runtime)
{
RuntimeDisconnected?.Invoke();
IsConnected = false;
}
}
protected override Response OnReceive(Message message)

View file

@ -20,6 +20,7 @@ using SafeExamBrowser.Communication.Proxies;
using SafeExamBrowser.Configuration.Cryptography;
using SafeExamBrowser.Contracts.Browser;
using SafeExamBrowser.Contracts.Client;
using SafeExamBrowser.Contracts.Communication;
using SafeExamBrowser.Contracts.Communication.Hosts;
using SafeExamBrowser.Contracts.Communication.Proxies;
using SafeExamBrowser.Contracts.Configuration;
@ -97,7 +98,7 @@ namespace SafeExamBrowser.Client
powerSupply = new PowerSupply(new ModuleLogger(logger, nameof(PowerSupply)), text);
processMonitor = new ProcessMonitor(new ModuleLogger(logger, nameof(ProcessMonitor)), nativeMethods);
uiFactory = BuildUserInterfaceFactory();
runtimeProxy = new RuntimeProxy(runtimeHostUri, new ProxyObjectFactory(), new ModuleLogger(logger, nameof(RuntimeProxy)));
runtimeProxy = new RuntimeProxy(runtimeHostUri, new ProxyObjectFactory(), new ModuleLogger(logger, nameof(RuntimeProxy)), Interlocutor.Client);
taskbar = BuildTaskbar();
terminationActivator = new TerminationActivator(new ModuleLogger(logger, nameof(TerminationActivator)));
windowMonitor = new WindowMonitor(new ModuleLogger(logger, nameof(WindowMonitor)), nativeMethods);

View file

@ -17,7 +17,6 @@ namespace SafeExamBrowser.Client.Operations
{
internal class RuntimeConnectionOperation : IOperation
{
private bool connected;
private ILogger logger;
private IRuntimeProxy runtime;
private Guid token;
@ -37,7 +36,7 @@ namespace SafeExamBrowser.Client.Operations
logger.Info("Initializing runtime connection...");
StatusChanged?.Invoke(TextKey.OperationStatus_InitializeRuntimeConnection);
connected = runtime.Connect(token);
var connected = runtime.Connect(token);
if (connected)
{
@ -56,7 +55,7 @@ namespace SafeExamBrowser.Client.Operations
logger.Info("Closing runtime connection...");
StatusChanged?.Invoke(TextKey.OperationStatus_CloseRuntimeConnection);
if (connected)
if (runtime.IsConnected)
{
var success = runtime.Disconnect();

View file

@ -7,7 +7,9 @@
*/
using System;
using System.Linq;
using SafeExamBrowser.Communication.Hosts;
using SafeExamBrowser.Contracts.Communication;
using SafeExamBrowser.Contracts.Communication.Data;
using SafeExamBrowser.Contracts.Communication.Hosts;
using SafeExamBrowser.Contracts.Logging;
@ -17,7 +19,7 @@ namespace SafeExamBrowser.Communication.UnitTests.Hosts
internal class BaseHostStub : BaseHost
{
public Func<Guid?, bool> OnConnectStub { get; set; }
public Action OnDisconnectStub { get; set; }
public Action<Interlocutor> OnDisconnectStub { get; set; }
public Func<Message, Response> OnReceiveStub { get; set; }
public Func<SimpleMessagePurport, Response> OnReceiveSimpleMessageStub { get; set; }
@ -27,7 +29,7 @@ namespace SafeExamBrowser.Communication.UnitTests.Hosts
public Guid? GetCommunicationToken()
{
return CommunicationToken;
return CommunicationToken.Any() ? CommunicationToken.First() : default(Guid?);
}
protected override bool OnConnect(Guid? token)
@ -35,9 +37,9 @@ namespace SafeExamBrowser.Communication.UnitTests.Hosts
return OnConnectStub?.Invoke(token) == true;
}
protected override void OnDisconnect()
protected override void OnDisconnect(Interlocutor interlocutor)
{
OnDisconnectStub?.Invoke();
OnDisconnectStub?.Invoke(interlocutor);
}
protected override Response OnReceive(Message message)

View file

@ -154,17 +154,19 @@ namespace SafeExamBrowser.Communication.UnitTests.Hosts
[TestMethod]
public void MustCorrectlyHandleDisconnectionRequest()
{
var message = new DisconnectionMessage();
var message = new DisconnectionMessage { Interlocutor = Interlocutor.Runtime };
var disconnected = false;
var interlocutor = Interlocutor.Unknown;
sut.OnConnectStub = (t) => { return true; };
sut.OnDisconnectStub = () => disconnected = true;
sut.OnDisconnectStub = (i) => { disconnected = true; interlocutor = i; };
sut.Connect();
message.CommunicationToken = sut.GetCommunicationToken().Value;
var response = sut.Disconnect(message);
Assert.AreEqual(message.Interlocutor, interlocutor);
Assert.IsTrue(disconnected);
Assert.IsTrue(response.ConnectionTerminated);
Assert.IsNull(sut.GetCommunicationToken());
@ -176,7 +178,7 @@ namespace SafeExamBrowser.Communication.UnitTests.Hosts
var disconnected = false;
sut.OnConnectStub = (t) => { return true; };
sut.OnDisconnectStub = () => disconnected = true;
sut.OnDisconnectStub = (i) => disconnected = true;
sut.Connect();
var response = sut.Disconnect(new DisconnectionMessage());

View file

@ -7,16 +7,17 @@
*/
using System;
using SafeExamBrowser.Communication.Proxies;
using SafeExamBrowser.Contracts.Communication;
using SafeExamBrowser.Contracts.Communication.Data;
using SafeExamBrowser.Contracts.Communication.Proxies;
using SafeExamBrowser.Contracts.Logging;
using SafeExamBrowser.Communication.Proxies;
namespace SafeExamBrowser.Communication.UnitTests.Proxies
{
internal class BaseProxyImpl : BaseProxy
{
public BaseProxyImpl(string address, IProxyObjectFactory factory, ILogger logger) : base(address, factory, logger)
public BaseProxyImpl(string address, IProxyObjectFactory factory, ILogger logger, Interlocutor owner) : base(address, factory, logger, owner)
{
}

View file

@ -10,6 +10,7 @@ using System;
using System.ServiceModel;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using SafeExamBrowser.Contracts.Communication;
using SafeExamBrowser.Contracts.Communication.Data;
using SafeExamBrowser.Contracts.Communication.Proxies;
using SafeExamBrowser.Contracts.Logging;
@ -29,7 +30,7 @@ namespace SafeExamBrowser.Communication.UnitTests.Proxies
proxyObjectFactory = new Mock<IProxyObjectFactory>();
logger = new Mock<ILogger>();
sut = new BaseProxyImpl("net.pipe://some/address/here", proxyObjectFactory.Object, logger.Object);
sut = new BaseProxyImpl("net.pipe://some/address/here", proxyObjectFactory.Object, logger.Object, default(Interlocutor));
}
[TestMethod]

View file

@ -11,6 +11,7 @@ using System.ServiceModel;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using SafeExamBrowser.Communication.Proxies;
using SafeExamBrowser.Contracts.Communication;
using SafeExamBrowser.Contracts.Communication.Data;
using SafeExamBrowser.Contracts.Communication.Proxies;
using SafeExamBrowser.Contracts.Logging;
@ -43,7 +44,7 @@ namespace SafeExamBrowser.Communication.UnitTests.Proxies
proxy.Setup(o => o.State).Returns(CommunicationState.Opened);
proxyObjectFactory.Setup(f => f.CreateObject(It.IsAny<string>())).Returns(proxy.Object);
sut = new ClientProxy("net.pipe://random/address/here", proxyObjectFactory.Object, logger.Object);
sut = new ClientProxy("net.pipe://random/address/here", proxyObjectFactory.Object, logger.Object, default(Interlocutor));
sut.Connect(Guid.NewGuid());
}

View file

@ -11,6 +11,7 @@ using System.ServiceModel;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using SafeExamBrowser.Communication.Proxies;
using SafeExamBrowser.Contracts.Communication;
using SafeExamBrowser.Contracts.Communication.Data;
using SafeExamBrowser.Contracts.Communication.Proxies;
using SafeExamBrowser.Contracts.Configuration;
@ -44,7 +45,7 @@ namespace SafeExamBrowser.Communication.UnitTests.Proxies
proxy.Setup(o => o.State).Returns(CommunicationState.Opened);
proxyObjectFactory.Setup(f => f.CreateObject(It.IsAny<string>())).Returns(proxy.Object);
sut = new RuntimeProxy("net.pipe://random/address/here", proxyObjectFactory.Object, logger.Object);
sut = new RuntimeProxy("net.pipe://random/address/here", proxyObjectFactory.Object, logger.Object, default(Interlocutor));
sut.Connect(Guid.NewGuid());
}

View file

@ -14,6 +14,7 @@ using SafeExamBrowser.Contracts.Communication.Data;
using SafeExamBrowser.Contracts.Communication.Proxies;
using SafeExamBrowser.Contracts.Logging;
using SafeExamBrowser.Communication.Proxies;
using SafeExamBrowser.Contracts.Communication;
namespace SafeExamBrowser.Communication.UnitTests.Proxies
{
@ -42,7 +43,7 @@ namespace SafeExamBrowser.Communication.UnitTests.Proxies
proxy.Setup(o => o.State).Returns(CommunicationState.Opened);
proxyObjectFactory.Setup(f => f.CreateObject(It.IsAny<string>())).Returns(proxy.Object);
sut = new ServiceProxy("net.pipe://random/address/here", proxyObjectFactory.Object, logger.Object);
sut = new ServiceProxy("net.pipe://random/address/here", proxyObjectFactory.Object, logger.Object, default(Interlocutor));
}
[TestMethod]

View file

@ -7,6 +7,7 @@
*/
using System;
using System.Collections.Generic;
using System.ServiceModel;
using System.Threading;
using SafeExamBrowser.Contracts.Communication;
@ -30,7 +31,7 @@ namespace SafeExamBrowser.Communication.Hosts
private Thread hostThread;
private int timeout_ms;
protected Guid? CommunicationToken { get; private set; }
protected IList<Guid> CommunicationToken { get; private set; }
protected ILogger Logger { get; private set; }
public bool IsRunning
@ -47,13 +48,14 @@ namespace SafeExamBrowser.Communication.Hosts
public BaseHost(string address, IHostObjectFactory factory, ILogger logger, int timeout_ms)
{
this.address = address;
this.CommunicationToken = new List<Guid>();
this.factory = factory;
this.Logger = logger;
this.timeout_ms = timeout_ms;
}
protected abstract bool OnConnect(Guid? token);
protected abstract void OnDisconnect();
protected abstract void OnDisconnect(Interlocutor interlocutor);
protected abstract Response OnReceive(Message message);
protected abstract Response OnReceive(SimpleMessagePurport message);
@ -61,15 +63,19 @@ namespace SafeExamBrowser.Communication.Hosts
{
lock (@lock)
{
Logger.Debug($"Received connection request with authentication token '{token}'.");
Logger.Debug($"Received connection request {(token.HasValue ? $"with authentication token '{token}'" : "without authentication token")}.");
var response = new ConnectionResponse();
var connected = OnConnect(token);
if (connected)
{
response.CommunicationToken = CommunicationToken = Guid.NewGuid();
var communicationToken = Guid.NewGuid();
response.CommunicationToken = communicationToken;
response.ConnectionEstablished = true;
CommunicationToken.Add(communicationToken);
}
Logger.Debug($"{(connected ? "Accepted" : "Denied")} connection request.");
@ -88,10 +94,10 @@ namespace SafeExamBrowser.Communication.Hosts
if (IsAuthorized(message?.CommunicationToken))
{
OnDisconnect();
OnDisconnect(message.Interlocutor);
CommunicationToken = null;
response.ConnectionTerminated = true;
CommunicationToken.Remove(message.CommunicationToken);
}
return response;
@ -166,7 +172,7 @@ namespace SafeExamBrowser.Communication.Hosts
private bool IsAuthorized(Guid? token)
{
return CommunicationToken == token;
return token.HasValue && CommunicationToken.Contains(token.Value);
}
private void TryStartHost(AutoResetEvent startedEvent, out Exception exception)

View file

@ -26,21 +26,23 @@ namespace SafeExamBrowser.Communication.Proxies
private readonly object @lock = new object();
private string address;
private Interlocutor owner;
private IProxyObject proxy;
private IProxyObjectFactory factory;
private Guid? communicationToken;
private Timer timer;
protected bool IsConnected { get { return communicationToken.HasValue; } }
protected ILogger Logger { get; private set; }
protected ILogger Logger { get; }
public bool IsConnected => communicationToken.HasValue;
public event CommunicationEventHandler ConnectionLost;
public BaseProxy(string address, IProxyObjectFactory factory, ILogger logger)
public BaseProxy(string address, IProxyObjectFactory factory, ILogger logger, Interlocutor owner)
{
this.address = address;
this.factory = factory;
this.Logger = logger;
this.owner = owner;
}
public virtual bool Connect(Guid? token = null, bool autoPing = true)
@ -89,7 +91,11 @@ namespace SafeExamBrowser.Communication.Proxies
StopAutoPing();
var message = new DisconnectionMessage { CommunicationToken = communicationToken.Value };
var message = new DisconnectionMessage
{
CommunicationToken = communicationToken.Value,
Interlocutor = owner
};
var response = proxy.Disconnect(message);
var success = response.ConnectionTerminated;

View file

@ -7,6 +7,7 @@
*/
using System;
using SafeExamBrowser.Contracts.Communication;
using SafeExamBrowser.Contracts.Communication.Data;
using SafeExamBrowser.Contracts.Communication.Proxies;
using SafeExamBrowser.Contracts.Logging;
@ -19,7 +20,7 @@ namespace SafeExamBrowser.Communication.Proxies
/// </summary>
public class ClientProxy : BaseProxy, IClientProxy
{
public ClientProxy(string address, IProxyObjectFactory factory, ILogger logger) : base(address, factory, logger)
public ClientProxy(string address, IProxyObjectFactory factory, ILogger logger, Interlocutor owner) : base(address, factory, logger, owner)
{
}

View file

@ -0,0 +1,34 @@
/*
* 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 SafeExamBrowser.Contracts.Communication;
using SafeExamBrowser.Contracts.Communication.Proxies;
using SafeExamBrowser.Contracts.Logging;
namespace SafeExamBrowser.Communication.Proxies
{
/// <summary>
/// Default implementation of the <see cref="IProxyFactory"/>, creating instances of the default proxy implementations.
/// </summary>
public class ProxyFactory : IProxyFactory
{
private IProxyObjectFactory factory;
private IModuleLogger logger;
public ProxyFactory(IProxyObjectFactory factory, IModuleLogger logger)
{
this.factory = factory;
this.logger = logger;
}
public IClientProxy CreateClientProxy(string address, Interlocutor owner)
{
return new ClientProxy(address, factory, logger.CloneFor(nameof(ClientProxy)), owner);
}
}
}

View file

@ -7,6 +7,7 @@
*/
using System;
using SafeExamBrowser.Contracts.Communication;
using SafeExamBrowser.Contracts.Communication.Data;
using SafeExamBrowser.Contracts.Communication.Proxies;
using SafeExamBrowser.Contracts.Logging;
@ -19,7 +20,7 @@ namespace SafeExamBrowser.Communication.Proxies
/// </summary>
public class RuntimeProxy : BaseProxy, IRuntimeProxy
{
public RuntimeProxy(string address, IProxyObjectFactory factory, ILogger logger) : base(address, factory, logger)
public RuntimeProxy(string address, IProxyObjectFactory factory, ILogger logger, Interlocutor owner) : base(address, factory, logger, owner)
{
}

View file

@ -7,6 +7,8 @@
*/
using System;
using SafeExamBrowser.Contracts.Communication;
using SafeExamBrowser.Contracts.Communication.Data;
using SafeExamBrowser.Contracts.Communication.Proxies;
using SafeExamBrowser.Contracts.Configuration;
using SafeExamBrowser.Contracts.Logging;
@ -19,9 +21,8 @@ namespace SafeExamBrowser.Communication.Proxies
public class ServiceProxy : BaseProxy, IServiceProxy
{
public bool Ignore { private get; set; }
public new bool IsConnected => base.IsConnected;
public ServiceProxy(string address, IProxyObjectFactory factory, ILogger logger) : base(address, factory, logger)
public ServiceProxy(string address, IProxyObjectFactory factory, ILogger logger, Interlocutor owner) : base(address, factory, logger, owner)
{
}
@ -52,10 +53,28 @@ namespace SafeExamBrowser.Communication.Proxies
return new CommunicationResult(true);
}
// Implement service communication...
// Send(new StartSessionMessage { Id = sessionId, Settings = settings });
try
{
var response = Send(new SessionStartMessage(configuration));
var success = IsAcknowledged(response);
throw new NotImplementedException();
if (success)
{
Logger.Debug("Service acknowledged session start.");
}
else
{
Logger.Error($"Service did not acknowledge session start! Received: {ToString(response)}.");
}
return new CommunicationResult(success);
}
catch (Exception e)
{
Logger.Error($"Failed to perform '{nameof(StartSession)}'", e);
return new CommunicationResult(false);
}
}
public CommunicationResult StopSession(Guid sessionId)
@ -65,10 +84,28 @@ namespace SafeExamBrowser.Communication.Proxies
return new CommunicationResult(true);
}
// Implement service communication...
// Send(new StopSessionMessage { SessionId = sessionId });
try
{
var response = Send(new SessionStopMessage(sessionId));
var success = IsAcknowledged(response);
throw new NotImplementedException();
if (success)
{
Logger.Debug("Service acknowledged session stop.");
}
else
{
Logger.Error($"Service did not acknowledge session stop! Received: {ToString(response)}.");
}
return new CommunicationResult(success);
}
catch (Exception e)
{
Logger.Error($"Failed to perform '{nameof(StopSession)}'", e);
return new CommunicationResult(false);
}
}
private bool IgnoreOperation(string operationName)

View file

@ -60,6 +60,7 @@
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Proxies\BaseProxy.cs" />
<Compile Include="Proxies\ClientProxy.cs" />
<Compile Include="Proxies\ProxyFactory.cs" />
<Compile Include="Proxies\ProxyObjectFactory.cs" />
<Compile Include="Proxies\RuntimeProxy.cs" />
<Compile Include="Proxies\ServiceProxy.cs" />

View file

@ -278,7 +278,6 @@ namespace SafeExamBrowser.Configuration.UnitTests
Assert.IsNull(configuration.Settings);
Assert.IsInstanceOfType(configuration.AppConfig, typeof(AppConfig));
Assert.IsInstanceOfType(configuration.ClientAuthenticationToken, typeof(Guid));
Assert.IsInstanceOfType(configuration.ServiceAuthenticationToken, typeof(Guid));
Assert.IsInstanceOfType(configuration.SessionId, typeof(Guid));
}
@ -292,14 +291,18 @@ namespace SafeExamBrowser.Configuration.UnitTests
var runtimeAddress = appConfig.RuntimeAddress;
var runtimeId = appConfig.RuntimeId;
var runtimeLogFilePath = appConfig.RuntimeLogFilePath;
var serviceEventName = appConfig.ServiceEventName;
var configuration = sut.InitializeSessionConfiguration();
Assert.AreNotEqual(configuration.AppConfig.ClientAddress, clientAddress);
Assert.AreNotEqual(configuration.AppConfig.ClientId, clientId);
Assert.AreEqual(configuration.AppConfig.ClientLogFilePath, clientLogFilePath);
Assert.AreEqual(configuration.AppConfig.RuntimeAddress, runtimeAddress);
Assert.AreEqual(configuration.AppConfig.RuntimeId, runtimeId);
Assert.AreEqual(configuration.AppConfig.RuntimeLogFilePath, runtimeLogFilePath);
Assert.AreNotEqual(configuration.AppConfig.ClientAddress, clientAddress);
Assert.AreNotEqual(configuration.AppConfig.ClientId, clientId);
Assert.AreNotEqual(configuration.AppConfig.ServiceEventName, serviceEventName);
}
[TestMethod]
@ -312,10 +315,8 @@ namespace SafeExamBrowser.Configuration.UnitTests
Assert.AreNotEqual(firstSession.SessionId, secondSession.SessionId);
Assert.AreNotEqual(firstSession.ClientAuthenticationToken, secondSession.ClientAuthenticationToken);
Assert.AreNotEqual(firstSession.ServiceAuthenticationToken, secondSession.ServiceAuthenticationToken);
Assert.AreNotEqual(secondSession.SessionId, thirdSession.SessionId);
Assert.AreNotEqual(secondSession.ClientAuthenticationToken, thirdSession.ClientAuthenticationToken);
Assert.AreNotEqual(secondSession.ServiceAuthenticationToken, thirdSession.ServiceAuthenticationToken);
}
private void RegisterModules()

View file

@ -78,10 +78,10 @@ namespace SafeExamBrowser.Configuration.ConfigurationData
appConfig.ClientId = Guid.NewGuid();
appConfig.ClientAddress = $"{BASE_ADDRESS}/client/{Guid.NewGuid()}";
appConfig.ServiceEventName = $@"Global\{nameof(SafeExamBrowser)}-{Guid.NewGuid()}";
configuration.AppConfig = appConfig.Clone();
configuration.ClientAuthenticationToken = Guid.NewGuid();
configuration.ServiceAuthenticationToken = Guid.NewGuid();
configuration.SessionId = Guid.NewGuid();
return configuration;

View file

@ -16,5 +16,9 @@ namespace SafeExamBrowser.Contracts.Communication.Data
[Serializable]
public class DisconnectionMessage : Message
{
/// <summary>
/// Identifies the component sending the message.
/// </summary>
public Interlocutor Interlocutor { get; set; }
}
}

View file

@ -0,0 +1,30 @@
/*
* 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 SafeExamBrowser.Contracts.Configuration;
namespace SafeExamBrowser.Contracts.Communication.Data
{
/// <summary>
/// This message is transmitted to the service to request the initialization of a new session.
/// </summary>
[Serializable]
public class SessionStartMessage : Message
{
/// <summary>
/// The configuration to be used by the service.
/// </summary>
public ServiceConfiguration Configuration { get; }
public SessionStartMessage(ServiceConfiguration configuration)
{
Configuration = configuration;
}
}
}

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/.
*/
using System;
namespace SafeExamBrowser.Contracts.Communication.Data
{
/// <summary>
/// This message is transmitted to the service to request the termination of a currently running session.
/// </summary>
[Serializable]
public class SessionStopMessage : Message
{
public Guid SessionId { get; }
public SessionStopMessage(Guid sessionId)
{
SessionId = sessionId;
}
}
}

View file

@ -0,0 +1,23 @@
/*
* 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 SafeExamBrowser.Contracts.Configuration;
namespace SafeExamBrowser.Contracts.Communication.Events
{
/// <summary>
/// The event arguments used for the session start event fired by the <see cref="Hosts.IServiceHost"/>.
/// </summary>
public class SessionStartEventArgs : CommunicationEventArgs
{
/// <summary>
/// The configuration to be used for the new session.
/// </summary>
public ServiceConfiguration Configuration { get; set; }
}
}

View file

@ -0,0 +1,23 @@
/*
* 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;
namespace SafeExamBrowser.Contracts.Communication.Events
{
/// <summary>
/// The event arguments used for the session stop event fired by the <see cref="Hosts.IServiceHost"/>.
/// </summary>
public class SessionStopEventArgs : CommunicationEventArgs
{
/// <summary>
/// The identifier of the session to be stopped.
/// </summary>
public Guid SessionId { get; set; }
}
}

View file

@ -24,7 +24,7 @@ namespace SafeExamBrowser.Contracts.Communication.Hosts
/// <summary>
/// The token used for initial authentication.
/// </summary>
Guid AuthenticationToken { set; }
Guid? AuthenticationToken { set; }
/// <summary>
/// Event fired when the client disconnected from the runtime.
@ -56,26 +56,6 @@ namespace SafeExamBrowser.Contracts.Communication.Hosts
/// </summary>
event CommunicationEventHandler<ReconfigurationEventArgs> ReconfigurationRequested;
/// <summary>
/// Event fired when the service disconnected from the runtime.
/// </summary>
event CommunicationEventHandler ServiceDisconnected;
/// <summary>
/// Event fired when the service has experienced a critical failure.
/// </summary>
event CommunicationEventHandler ServiceFailed;
/// <summary>
/// Event fired when the service has successfully started a new session.
/// </summary>
event CommunicationEventHandler ServiceSessionStarted;
/// <summary>
/// Event fired when the service has successfully stopped the currently running session.
/// </summary>
event CommunicationEventHandler ServiceSessionStopped;
/// <summary>
/// Event fired when the client requests to shut down the application.
/// </summary>

View file

@ -6,6 +6,8 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using SafeExamBrowser.Contracts.Communication.Events;
namespace SafeExamBrowser.Contracts.Communication.Hosts
{
/// <summary>
@ -13,5 +15,19 @@ namespace SafeExamBrowser.Contracts.Communication.Hosts
/// </summary>
public interface IServiceHost : ICommunicationHost
{
/// <summary>
/// Determines whether another application component may establish a connection with the host.
/// </summary>
bool AllowConnection { get; set; }
/// <summary>
/// Event fired when the runtime requested to start a new session.
/// </summary>
event CommunicationEventHandler<SessionStartEventArgs> SessionStartRequested;
/// <summary>
/// Event fired when the runtime requested to stop a running session.
/// </summary>
event CommunicationEventHandler<SessionStopEventArgs> SessionStopRequested;
}
}

View file

@ -28,6 +28,8 @@ namespace SafeExamBrowser.Contracts.Communication
[ServiceKnownType(typeof(ReconfigurationMessage))]
[ServiceKnownType(typeof(ReconfigurationDeniedMessage))]
[ServiceKnownType(typeof(ServiceConfiguration))]
[ServiceKnownType(typeof(SessionStartMessage))]
[ServiceKnownType(typeof(SessionStopMessage))]
[ServiceKnownType(typeof(SimpleMessage))]
[ServiceKnownType(typeof(SimpleResponse))]
public interface ICommunication

View file

@ -17,6 +17,11 @@ namespace SafeExamBrowser.Contracts.Communication
/// </summary>
public interface ICommunicationProxy
{
/// <summary>
/// Indicates whether a connection to the host has been established.
/// </summary>
bool IsConnected { get; }
/// <summary>
/// Fired when the connection to the host was lost, e.g. if a ping request failed or a communication fault occurred.
/// </summary>

View file

@ -0,0 +1,39 @@
/*
* 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;
namespace SafeExamBrowser.Contracts.Communication
{
/// <summary>
/// Defines all possible interlocutors for inter-process communication within the application.
/// </summary>
[Serializable]
public enum Interlocutor
{
/// <summary>
/// The interlocutor is not an application component.
/// </summary>
Unknown = 0,
/// <summary>
/// The client application component.
/// </summary>
Client,
/// <summary>
/// The runtime application component.
/// </summary>
Runtime,
/// <summary>
/// The service application component.
/// </summary>
Service
}
}

View file

@ -14,8 +14,8 @@ namespace SafeExamBrowser.Contracts.Communication.Proxies
public interface IProxyFactory
{
/// <summary>
/// Creates a new <see cref="IClientProxy"/> for the given endpoint address.
/// Creates a new <see cref="IClientProxy"/> for the given endpoint address and owner.
/// </summary>
IClientProxy CreateClientProxy(string address);
IClientProxy CreateClientProxy(string address, Interlocutor owner);
}
}

View file

@ -22,11 +22,6 @@ namespace SafeExamBrowser.Contracts.Communication.Proxies
/// </summary>
bool Ignore { set; }
/// <summary>
/// Indicates whether a connection to the communication host of the service has been established.
/// </summary>
bool IsConnected { get; }
/// <summary>
/// Instructs the service to start a new session according to the given configuration.
/// </summary>

View file

@ -116,6 +116,11 @@ namespace SafeExamBrowser.Contracts.Configuration
/// </summary>
public string ServiceAddress { get; set; }
/// <summary>
/// The name of the global inter-process synchronization event hosted by the service.
/// </summary>
public string ServiceEventName { get; set; }
/// <summary>
/// Creates a shallow clone.
/// </summary>

View file

@ -21,11 +21,6 @@ namespace SafeExamBrowser.Contracts.Configuration
/// </summary>
public AppConfig AppConfig { get; set; }
/// <summary>
/// The token used for initial authentication with the runtime.
/// </summary>
public Guid AuthenticationToken { get; set; }
/// <summary>
/// The unique identifier for the current session.
/// </summary>

View file

@ -25,11 +25,6 @@ namespace SafeExamBrowser.Contracts.Configuration
/// </summary>
public Guid ClientAuthenticationToken { get; set; }
/// <summary>
/// The token used for initial communication authentication with the service.
/// </summary>
public Guid ServiceAuthenticationToken { get; set; }
/// <summary>
/// The unique session identifier.
/// </summary>

View file

@ -56,10 +56,15 @@
<Compile Include="Browser\ProgressChangedEventHandler.cs" />
<Compile Include="Communication\Data\MessageBoxReplyMessage.cs" />
<Compile Include="Communication\Data\MessageBoxRequestMessage.cs" />
<Compile Include="Communication\Data\SessionStartMessage.cs" />
<Compile Include="Communication\Data\SessionStopMessage.cs" />
<Compile Include="Communication\Events\ClientConfigurationEventArgs.cs" />
<Compile Include="Communication\Events\MessageBoxReplyEventArgs.cs" />
<Compile Include="Communication\Events\MessageBoxRequestEventArgs.cs" />
<Compile Include="Communication\Events\SessionStartEventArgs.cs" />
<Compile Include="Communication\Events\SessionStopEventArgs.cs" />
<Compile Include="Communication\Hosts\IServiceHost.cs" />
<Compile Include="Communication\Interlocutor.cs" />
<Compile Include="Configuration\ClientConfiguration.cs" />
<Compile Include="Configuration\Cryptography\EncryptionParameters.cs" />
<Compile Include="Configuration\Cryptography\ICertificateStore.cs" />

View file

@ -43,7 +43,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Communication
}
[TestMethod]
public void MustOnlyAllowConnectionIfTokenIsValid()
public void MustAllowConnectionIfTokenIsValid()
{
var token = Guid.NewGuid();
@ -70,6 +70,19 @@ namespace SafeExamBrowser.Runtime.UnitTests.Communication
Assert.IsFalse(response.ConnectionEstablished);
}
[TestMethod]
public void MustRejectConnectionIfNoAuthenticationTokenSet()
{
var token = Guid.Empty;
sut.AllowConnection = true;
var response = sut.Connect(token);
Assert.IsNotNull(response);
Assert.IsFalse(response.ConnectionEstablished);
}
[TestMethod]
public void MustOnlyAllowOneConcurrentConnection()
{
@ -94,7 +107,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Communication
}
[TestMethod]
public void MustCorrectlyDisconnect()
public void MustCorrectlyDisconnectClient()
{
var disconnected = false;
var token = Guid.NewGuid();
@ -104,7 +117,12 @@ namespace SafeExamBrowser.Runtime.UnitTests.Communication
sut.ClientDisconnected += () => disconnected = true;
var connectionResponse = sut.Connect(token);
var response = sut.Disconnect(new DisconnectionMessage { CommunicationToken = connectionResponse.CommunicationToken.Value });
var message = new DisconnectionMessage
{
CommunicationToken = connectionResponse.CommunicationToken.Value,
Interlocutor = Interlocutor.Client
};
var response = sut.Disconnect(message);
Assert.IsNotNull(response);
Assert.IsTrue(disconnected);

View file

@ -10,6 +10,7 @@ using System;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using SafeExamBrowser.Contracts.Communication;
using SafeExamBrowser.Contracts.Communication.Data;
using SafeExamBrowser.Contracts.Communication.Hosts;
using SafeExamBrowser.Contracts.Communication.Proxies;
@ -63,7 +64,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
session.Settings = settings;
sessionContext.Current = session;
sessionContext.Next = session;
proxyFactory.Setup(f => f.CreateClientProxy(It.IsAny<string>())).Returns(proxy.Object);
proxyFactory.Setup(f => f.CreateClientProxy(It.IsAny<string>(), It.IsAny<Interlocutor>())).Returns(proxy.Object);
sut = new ClientOperation(logger.Object, processFactory.Object, proxyFactory.Object, runtimeHost.Object, sessionContext, 0);
}

View file

@ -9,6 +9,7 @@
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using SafeExamBrowser.Contracts.Communication;
using SafeExamBrowser.Contracts.Communication.Hosts;
using SafeExamBrowser.Contracts.Communication.Proxies;
using SafeExamBrowser.Contracts.Configuration;
@ -60,7 +61,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
sessionContext.ClientProxy = proxy.Object;
sessionContext.Current = session;
sessionContext.Next = session;
proxyFactory.Setup(f => f.CreateClientProxy(It.IsAny<string>())).Returns(proxy.Object);
proxyFactory.Setup(f => f.CreateClientProxy(It.IsAny<string>(), It.IsAny<Interlocutor>())).Returns(proxy.Object);
sut = new ClientTerminationOperation(logger.Object, processFactory.Object, proxyFactory.Object, runtimeHost.Object, sessionContext, 0);
}

View file

@ -7,6 +7,7 @@
*/
using System;
using System.Threading;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using SafeExamBrowser.Contracts.Communication.Hosts;
@ -24,9 +25,11 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
[TestClass]
public class ServiceOperationTests
{
private AppConfig appConfig;
private Mock<ILogger> logger;
private Mock<IRuntimeHost> runtimeHost;
private Mock<IServiceProxy> service;
private EventWaitHandle serviceEvent;
private SessionConfiguration session;
private SessionContext sessionContext;
private Settings settings;
@ -35,15 +38,22 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
[TestInitialize]
public void Initialize()
{
var serviceEventName = $"{nameof(SafeExamBrowser)}-{nameof(ServiceOperationTests)}";
appConfig = new AppConfig();
logger = new Mock<ILogger>();
runtimeHost = new Mock<IRuntimeHost>();
service = new Mock<IServiceProxy>();
serviceEvent = new EventWaitHandle(false, EventResetMode.AutoReset, serviceEventName);
session = new SessionConfiguration();
sessionContext = new SessionContext();
settings = new Settings();
appConfig.ServiceEventName = serviceEventName;
sessionContext.Current = session;
sessionContext.Current.AppConfig = appConfig;
sessionContext.Next = session;
sessionContext.Next.AppConfig = appConfig;
session.Settings = settings;
settings.ServicePolicy = ServicePolicy.Mandatory;
@ -71,10 +81,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
{
service.SetupGet(s => s.IsConnected).Returns(true);
service.Setup(s => s.Connect(null, true)).Returns(true);
service
.Setup(s => s.StartSession(It.IsAny<ServiceConfiguration>()))
.Returns(new CommunicationResult(true))
.Callback(() => runtimeHost.Raise(h => h.ServiceSessionStarted += null));
service.Setup(s => s.StartSession(It.IsAny<ServiceConfiguration>())).Returns(new CommunicationResult(true)).Callback(() => serviceEvent.Set());
var result = sut.Perform();
@ -88,10 +95,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
{
service.SetupGet(s => s.IsConnected).Returns(true);
service.Setup(s => s.Connect(null, true)).Returns(true);
service
.Setup(s => s.StartSession(It.IsAny<ServiceConfiguration>()))
.Returns(new CommunicationResult(true))
.Callback(() => runtimeHost.Raise(h => h.ServiceFailed += null));
service.Setup(s => s.StartSession(It.IsAny<ServiceConfiguration>())).Returns(new CommunicationResult(true));
var result = sut.Perform();
@ -197,10 +201,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
[TestMethod]
public void Repeat_MustStopCurrentAndStartNewSession()
{
service
.Setup(s => s.StopSession(It.IsAny<Guid>()))
.Returns(new CommunicationResult(true))
.Callback(() => runtimeHost.Raise(h => h.ServiceSessionStopped += null));
service.Setup(s => s.StopSession(It.IsAny<Guid>())).Returns(new CommunicationResult(true)).Callback(() => serviceEvent.Set());
PerformNormally();
@ -222,10 +223,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
service.Reset();
service.SetupGet(s => s.IsConnected).Returns(false);
service.Setup(s => s.Connect(null, true)).Returns(true);
service
.Setup(s => s.StopSession(It.IsAny<Guid>()))
.Returns(new CommunicationResult(true))
.Callback(() => runtimeHost.Raise(h => h.ServiceSessionStopped += null));
service.Setup(s => s.StopSession(It.IsAny<Guid>())).Returns(new CommunicationResult(true));
var result = sut.Repeat();
@ -251,58 +249,49 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
}
[TestMethod]
public void Revert_MustDisconnect()
{
service.Setup(s => s.Disconnect()).Returns(true).Callback(() => runtimeHost.Raise(h => h.ServiceDisconnected += null));
service
.Setup(s => s.StopSession(It.IsAny<Guid>()))
.Returns(new CommunicationResult(true))
.Callback(() => runtimeHost.Raise(h => h.ServiceSessionStopped += null));
PerformNormally();
var result = sut.Revert();
service.Verify(s => s.Disconnect(), Times.Once);
Assert.AreEqual(OperationResult.Success, result);
}
[TestMethod]
public void Revert_MustFailIfServiceNotDisconnectedWithinTimeout()
public void Repeat_MustFailIfSessionNotStoppedWithinTimeout()
{
const int TIMEOUT = 50;
var after = default(DateTime);
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);
service.Setup(s => s.Disconnect()).Returns(true);
service
.Setup(s => s.StopSession(It.IsAny<Guid>()))
.Returns(new CommunicationResult(true))
.Callback(() => runtimeHost.Raise(h => h.ServiceSessionStopped += null));
PerformNormally();
before = DateTime.Now;
var result = sut.Revert();
var result = sut.Repeat();
after = DateTime.Now;
service.Verify(s => s.Disconnect(), Times.Once);
service.Verify(s => s.StopSession(It.IsAny<Guid>()), Times.Once);
service.Verify(s => s.Disconnect(), Times.Never);
Assert.AreEqual(OperationResult.Failed, result);
Assert.IsTrue(after - before >= new TimeSpan(0, 0, 0, 0, TIMEOUT));
}
[TestMethod]
public void Revert_MustDisconnect()
{
service.Setup(s => s.Disconnect()).Returns(true);
service.Setup(s => s.StopSession(It.IsAny<Guid>())).Returns(new CommunicationResult(true)).Callback(() => serviceEvent.Set());
PerformNormally();
var result = sut.Revert();
service.Verify(s => s.Disconnect(), Times.Once);
Assert.AreEqual(OperationResult.Success, result);
}
[TestMethod]
public void Revert_MustStopSessionIfConnected()
{
service.Setup(s => s.Disconnect()).Returns(true).Callback(() => runtimeHost.Raise(h => h.ServiceDisconnected += null));
service
.Setup(s => s.StopSession(It.IsAny<Guid>()))
.Returns(new CommunicationResult(true))
.Callback(() => runtimeHost.Raise(h => h.ServiceSessionStopped += null));
service.Setup(s => s.Disconnect()).Returns(true);
service.Setup(s => s.StopSession(It.IsAny<Guid>())).Returns(new CommunicationResult(true)).Callback(() => serviceEvent.Set());
PerformNormally();
@ -332,10 +321,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
[TestMethod]
public void Revert_MustFailIfSessionStopUnsuccessful()
{
service
.Setup(s => s.StopSession(It.IsAny<Guid>()))
.Returns(new CommunicationResult(true))
.Callback(() => runtimeHost.Raise(h => h.ServiceFailed += null));
service.Setup(s => s.StopSession(It.IsAny<Guid>())).Returns(new CommunicationResult(true));
PerformNormally();
@ -377,6 +363,20 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
Assert.AreEqual(OperationResult.Success, result);
}
[TestMethod]
public void Revert_MustNotStopSessionIfNoSessionRunning()
{
sessionContext.Current = null;
service.SetupGet(s => s.IsConnected).Returns(true);
service.Setup(s => s.Disconnect()).Returns(true);
var result = sut.Revert();
service.Verify(s => s.StopSession(It.IsAny<Guid>()), Times.Never);
Assert.AreEqual(OperationResult.Success, result);
}
[TestMethod]
public void Revert_MustNotDisconnnectIfNotConnected()
{
@ -390,10 +390,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
{
service.SetupGet(s => s.IsConnected).Returns(true);
service.Setup(s => s.Connect(null, true)).Returns(true);
service
.Setup(s => s.StartSession(It.IsAny<ServiceConfiguration>()))
.Returns(new CommunicationResult(true))
.Callback(() => runtimeHost.Raise(h => h.ServiceSessionStarted += null));
service.Setup(s => s.StartSession(It.IsAny<ServiceConfiguration>())).Returns(new CommunicationResult(true)).Callback(() => serviceEvent.Set());
sut.Perform();
}

View file

@ -11,6 +11,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using SafeExamBrowser.Contracts.Communication.Hosts;
using SafeExamBrowser.Contracts.Configuration;
using SafeExamBrowser.Contracts.Core.OperationModel;
using SafeExamBrowser.Contracts.Logging;
using SafeExamBrowser.Runtime.Operations;
@ -52,33 +53,41 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
session.ClientAuthenticationToken = token;
sut.Perform();
var result = sut.Perform();
configuration.Verify(c => c.InitializeSessionConfiguration(), Times.Once);
runtimeHost.VerifySet(r => r.AuthenticationToken = token, Times.Once);
Assert.AreEqual(OperationResult.Success, result);
Assert.IsNull(sessionContext.Current);
}
[TestMethod]
public void MustInitializeConfigurationOnRepeat()
{
var currentSession = new SessionConfiguration();
var token = Guid.NewGuid();
session.ClientAuthenticationToken = token;
sessionContext.Current = currentSession;
sut.Repeat();
var result = sut.Repeat();
configuration.Verify(c => c.InitializeSessionConfiguration(), Times.Once);
runtimeHost.VerifySet(r => r.AuthenticationToken = token, Times.Once);
Assert.AreEqual(OperationResult.Success, result);
Assert.AreSame(currentSession,sessionContext.Current);
}
[TestMethod]
public void MustDoNothingOnRevert()
{
sut.Revert();
var result = sut.Revert();
configuration.VerifyNoOtherCalls();
logger.VerifyNoOtherCalls();
runtimeHost.VerifyNoOtherCalls();
Assert.AreEqual(OperationResult.Success, result);
}
}
}

View file

@ -1,32 +0,0 @@
/*
* 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 SafeExamBrowser.Contracts.Communication.Proxies;
using SafeExamBrowser.Contracts.Logging;
using SafeExamBrowser.Communication.Proxies;
using SafeExamBrowser.Logging;
namespace SafeExamBrowser.Runtime.Communication
{
internal class ProxyFactory : IProxyFactory
{
private IProxyObjectFactory factory;
private ILogger logger;
public ProxyFactory(IProxyObjectFactory factory, ILogger logger)
{
this.factory = factory;
this.logger = logger;
}
public IClientProxy CreateClientProxy(string address)
{
return new ClientProxy(address, factory, new ModuleLogger(logger, nameof(ClientProxy)));
}
}
}

View file

@ -8,6 +8,7 @@
using System;
using SafeExamBrowser.Communication.Hosts;
using SafeExamBrowser.Contracts.Communication;
using SafeExamBrowser.Contracts.Communication.Data;
using SafeExamBrowser.Contracts.Communication.Events;
using SafeExamBrowser.Contracts.Communication.Hosts;
@ -18,7 +19,7 @@ namespace SafeExamBrowser.Runtime.Communication
internal class RuntimeHost : BaseHost, IRuntimeHost
{
public bool AllowConnection { get; set; }
public Guid AuthenticationToken { private get; set; }
public Guid? AuthenticationToken { private get; set; }
public event CommunicationEventHandler ClientDisconnected;
public event CommunicationEventHandler ClientReady;
@ -26,10 +27,6 @@ namespace SafeExamBrowser.Runtime.Communication
public event CommunicationEventHandler<MessageBoxReplyEventArgs> MessageBoxReplyReceived;
public event CommunicationEventHandler<PasswordReplyEventArgs> PasswordReceived;
public event CommunicationEventHandler<ReconfigurationEventArgs> ReconfigurationRequested;
public event CommunicationEventHandler ServiceDisconnected;
public event CommunicationEventHandler ServiceFailed;
public event CommunicationEventHandler ServiceSessionStarted;
public event CommunicationEventHandler ServiceSessionStopped;
public event CommunicationEventHandler ShutdownRequested;
public RuntimeHost(string address, IHostObjectFactory factory, ILogger logger, int timeout_ms) : base(address, factory, logger, timeout_ms)
@ -38,7 +35,7 @@ namespace SafeExamBrowser.Runtime.Communication
protected override bool OnConnect(Guid? token = null)
{
var authenticated = AuthenticationToken == token;
var authenticated = AuthenticationToken.HasValue && AuthenticationToken == token;
var accepted = AllowConnection && authenticated;
if (accepted)
@ -49,9 +46,12 @@ namespace SafeExamBrowser.Runtime.Communication
return accepted;
}
protected override void OnDisconnect()
protected override void OnDisconnect(Interlocutor interlocutor)
{
ClientDisconnected?.Invoke();
if (interlocutor == Interlocutor.Client)
{
ClientDisconnected?.Invoke();
}
}
protected override Response OnReceive(Message message)

View file

@ -17,6 +17,7 @@ using SafeExamBrowser.Configuration.Cryptography;
using SafeExamBrowser.Configuration.DataCompression;
using SafeExamBrowser.Configuration.DataFormats;
using SafeExamBrowser.Configuration.DataResources;
using SafeExamBrowser.Contracts.Communication;
using SafeExamBrowser.Contracts.Configuration;
using SafeExamBrowser.Contracts.Core.OperationModel;
using SafeExamBrowser.Contracts.I18n;
@ -65,9 +66,9 @@ namespace SafeExamBrowser.Runtime
var desktopFactory = new DesktopFactory(ModuleLogger(nameof(DesktopFactory)));
var explorerShell = new ExplorerShell(ModuleLogger(nameof(ExplorerShell)), nativeMethods);
var processFactory = new ProcessFactory(ModuleLogger(nameof(ProcessFactory)));
var proxyFactory = new ProxyFactory(new ProxyObjectFactory(), logger);
var proxyFactory = new ProxyFactory(new ProxyObjectFactory(), ModuleLogger(nameof(ProxyFactory)));
var runtimeHost = new RuntimeHost(appConfig.RuntimeAddress, new HostObjectFactory(), ModuleLogger(nameof(RuntimeHost)), FIVE_SECONDS);
var serviceProxy = new ServiceProxy(appConfig.ServiceAddress, new ProxyObjectFactory(), ModuleLogger(nameof(ServiceProxy)));
var serviceProxy = new ServiceProxy(appConfig.ServiceAddress, new ProxyObjectFactory(), ModuleLogger(nameof(ServiceProxy)), Interlocutor.Runtime);
var sessionContext = new SessionContext();
var uiFactory = new UserInterfaceFactory(text);

View file

@ -6,7 +6,9 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Threading;
using SafeExamBrowser.Contracts.Communication;
using SafeExamBrowser.Contracts.Communication.Events;
using SafeExamBrowser.Contracts.Communication.Hosts;
using SafeExamBrowser.Contracts.Communication.Proxies;
@ -111,6 +113,7 @@ namespace SafeExamBrowser.Runtime.Operations
logger.Info("Starting new client process...");
runtimeHost.AllowConnection = true;
runtimeHost.AuthenticationToken = Context.Next.ClientAuthenticationToken;
runtimeHost.ClientReady += clientReadyEventHandler;
ClientProcess = processFactory.StartNew(executablePath, logFilePath, logLevel, runtimeHostUri, authenticationToken, uiMode);
ClientProcess.Terminated += clientTerminatedEventHandler;
@ -119,6 +122,7 @@ namespace SafeExamBrowser.Runtime.Operations
clientReady = clientReadyEvent.WaitOne(timeout_ms);
runtimeHost.AllowConnection = false;
runtimeHost.AuthenticationToken = default(Guid?);
runtimeHost.ClientReady -= clientReadyEventHandler;
ClientProcess.Terminated -= clientTerminatedEventHandler;
@ -145,7 +149,7 @@ namespace SafeExamBrowser.Runtime.Operations
var success = false;
logger.Info("Client has been successfully started and initialized. Creating communication proxy for client host...");
ClientProxy = proxyFactory.CreateClientProxy(Context.Next.AppConfig.ClientAddress);
ClientProxy = proxyFactory.CreateClientProxy(Context.Next.AppConfig.ClientAddress, Interlocutor.Runtime);
if (ClientProxy.Connect(Context.Next.ClientAuthenticationToken))
{

View file

@ -6,8 +6,9 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Security.AccessControl;
using System.Threading;
using SafeExamBrowser.Contracts.Communication.Events;
using SafeExamBrowser.Contracts.Communication.Hosts;
using SafeExamBrowser.Contracts.Communication.Proxies;
using SafeExamBrowser.Contracts.Configuration;
@ -46,7 +47,7 @@ namespace SafeExamBrowser.Runtime.Operations
public override OperationResult Perform()
{
logger.Info($"Initializing service session...");
logger.Info($"Initializing service...");
StatusChanged?.Invoke(TextKey.OperationStatus_InitializeServiceSession);
var success = TryEstablishConnection();
@ -61,7 +62,7 @@ namespace SafeExamBrowser.Runtime.Operations
public override OperationResult Repeat()
{
logger.Info($"Initializing new service session...");
logger.Info($"Initializing service...");
StatusChanged?.Invoke(TextKey.OperationStatus_InitializeServiceSession);
var success = false;
@ -85,14 +86,18 @@ namespace SafeExamBrowser.Runtime.Operations
public override OperationResult Revert()
{
logger.Info("Finalizing service session...");
logger.Info("Finalizing service...");
StatusChanged?.Invoke(TextKey.OperationStatus_FinalizeServiceSession);
var success = true;
if (service.IsConnected)
{
success &= TryStopSession();
if (Context.Current != null)
{
success &= TryStopSession();
}
success &= TryTerminateConnection();
}
@ -137,36 +142,18 @@ namespace SafeExamBrowser.Runtime.Operations
private bool TryTerminateConnection()
{
var serviceEvent = new AutoResetEvent(false);
var serviceEventHandler = new CommunicationEventHandler(() => serviceEvent.Set());
var disconnected = service.Disconnect();
runtimeHost.ServiceDisconnected += serviceEventHandler;
var success = service.Disconnect();
if (success)
if (disconnected)
{
logger.Info("Successfully disconnected from service. Waiting for service to disconnect...");
success = serviceEvent.WaitOne(timeout_ms);
if (success)
{
logger.Info("Service disconnected successfully.");
}
else
{
logger.Error($"Service failed to disconnect within {timeout_ms / 1000} seconds!");
}
logger.Info("Successfully disconnected from service.");
}
else
{
logger.Error("Failed to disconnect from service!");
}
runtimeHost.ServiceDisconnected -= serviceEventHandler;
return success;
return disconnected;
}
private bool TryStartSession()
@ -174,18 +161,10 @@ namespace SafeExamBrowser.Runtime.Operations
var configuration = new ServiceConfiguration
{
AppConfig = Context.Next.AppConfig,
AuthenticationToken = Context.Next.ServiceAuthenticationToken,
SessionId = Context.Next.SessionId,
Settings = Context.Next.Settings
};
var failure = false;
var success = false;
var serviceEvent = new AutoResetEvent(false);
var failureEventHandler = new CommunicationEventHandler(() => { failure = true; serviceEvent.Set(); });
var successEventHandler = new CommunicationEventHandler(() => { success = true; serviceEvent.Set(); });
runtimeHost.ServiceFailed += failureEventHandler;
runtimeHost.ServiceSessionStarted += successEventHandler;
var started = false;
logger.Info("Starting new service session...");
@ -193,16 +172,12 @@ namespace SafeExamBrowser.Runtime.Operations
if (communication.Success)
{
serviceEvent.WaitOne(timeout_ms);
started = TryWaitForServiceEvent(Context.Next.AppConfig.ServiceEventName);
if (success)
if (started)
{
logger.Info("Successfully started new service session.");
}
else if (failure)
{
logger.Error("An error occurred while attempting to start a new service session! Please check the service log for further information.");
}
else
{
logger.Error($"Failed to start new service session within {timeout_ms / 1000} seconds!");
@ -213,22 +188,12 @@ namespace SafeExamBrowser.Runtime.Operations
logger.Error("Failed to communicate session start to service!");
}
runtimeHost.ServiceFailed -= failureEventHandler;
runtimeHost.ServiceSessionStarted -= successEventHandler;
return success;
return started;
}
private bool TryStopSession()
{
var failure = false;
var success = false;
var serviceEvent = new AutoResetEvent(false);
var failureEventHandler = new CommunicationEventHandler(() => { failure = true; serviceEvent.Set(); });
var successEventHandler = new CommunicationEventHandler(() => { success = true; serviceEvent.Set(); });
runtimeHost.ServiceFailed += failureEventHandler;
runtimeHost.ServiceSessionStopped += successEventHandler;
var stopped = false;
logger.Info("Stopping current service session...");
@ -236,16 +201,12 @@ namespace SafeExamBrowser.Runtime.Operations
if (communication.Success)
{
serviceEvent.WaitOne(timeout_ms);
stopped = TryWaitForServiceEvent(Context.Current.AppConfig.ServiceEventName);
if (success)
if (stopped)
{
logger.Info("Successfully stopped service session.");
}
else if (failure)
{
logger.Error("An error occurred while attempting to stop the current service session! Please check the service log for further information.");
}
else
{
logger.Error($"Failed to stop service session within {timeout_ms / 1000} seconds!");
@ -256,10 +217,31 @@ namespace SafeExamBrowser.Runtime.Operations
logger.Error("Failed to communicate session stop to service!");
}
runtimeHost.ServiceFailed -= failureEventHandler;
runtimeHost.ServiceSessionStopped -= successEventHandler;
return stopped;
}
return success;
private bool TryWaitForServiceEvent(string eventName)
{
var serviceEvent = default(EventWaitHandle);
var startTime = DateTime.Now;
do
{
if (EventWaitHandle.TryOpenExisting(eventName, EventWaitHandleRights.Synchronize, out serviceEvent))
{
break;
}
} while (startTime.AddMilliseconds(timeout_ms) > DateTime.Now);
if (serviceEvent != default(EventWaitHandle))
{
using (serviceEvent)
{
return serviceEvent.WaitOne(timeout_ms);
}
}
return false;
}
}
}

View file

@ -6,7 +6,6 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using SafeExamBrowser.Contracts.Communication.Hosts;
using SafeExamBrowser.Contracts.Configuration;
using SafeExamBrowser.Contracts.Core.OperationModel;
@ -63,7 +62,6 @@ namespace SafeExamBrowser.Runtime.Operations
StatusChanged?.Invoke(TextKey.OperationStatus_InitializeSession);
Context.Next = configuration.InitializeSessionConfiguration();
runtimeHost.AuthenticationToken = Context.Next.ClientAuthenticationToken;
logger.Info($" -> Client-ID: {Context.Next.AppConfig.ClientId}");
logger.Info($" -> Runtime-ID: {Context.Next.AppConfig.RuntimeId}");

View file

@ -1,4 +1,12 @@
using SafeExamBrowser.Contracts.Core.OperationModel;
/*
* 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 SafeExamBrowser.Contracts.Core.OperationModel;
using SafeExamBrowser.Contracts.Core.OperationModel.Events;
namespace SafeExamBrowser.Runtime.Operations

View file

@ -103,7 +103,6 @@
<Compile Include="Operations\SessionActivationOperation.cs" />
<Compile Include="Operations\SessionOperation.cs" />
<Compile Include="Operations\SessionInitializationOperation.cs" />
<Compile Include="Communication\ProxyFactory.cs" />
<Compile Include="Communication\RuntimeHost.cs" />
<Compile Include="CompositionRoot.cs" />
<Compile Include="Properties\AssemblyInfo.cs">

View file

@ -11,12 +11,14 @@ using Moq;
using SafeExamBrowser.Contracts.Communication.Hosts;
using SafeExamBrowser.Contracts.Configuration;
using SafeExamBrowser.Contracts.Core.OperationModel;
using SafeExamBrowser.Contracts.Logging;
namespace SafeExamBrowser.Service.UnitTests
{
[TestClass]
public class ServiceControllerTests
{
private Mock<ILogger> logger;
private Mock<IOperationSequence> bootstrapSequence;
private SessionContext sessionContext;
private Mock<IRepeatableOperationSequence> sessionSequence;
@ -26,12 +28,13 @@ namespace SafeExamBrowser.Service.UnitTests
[TestInitialize]
public void Initialize()
{
logger = new Mock<ILogger>();
bootstrapSequence = new Mock<IOperationSequence>();
sessionContext = new SessionContext();
sessionSequence = new Mock<IRepeatableOperationSequence>();
serviceHost = new Mock<IServiceHost>();
sut = new ServiceController(bootstrapSequence.Object, sessionSequence.Object, serviceHost.Object, sessionContext);
sut = new ServiceController(logger.Object, bootstrapSequence.Object, sessionSequence.Object, serviceHost.Object, sessionContext);
}
[TestMethod]
@ -39,7 +42,7 @@ namespace SafeExamBrowser.Service.UnitTests
{
bootstrapSequence.Setup(b => b.TryPerform()).Returns(OperationResult.Success);
sessionSequence.Setup(b => b.TryPerform()).Returns(OperationResult.Success);
sessionContext.Current = null;
sessionContext.Configuration = null;
var success = sut.TryStart();
@ -66,7 +69,7 @@ namespace SafeExamBrowser.Service.UnitTests
bootstrapSequence.Setup(b => b.TryRevert()).Returns(OperationResult.Success).Callback(() => bootstrap = ++order);
sessionSequence.Setup(b => b.TryRevert()).Returns(OperationResult.Success).Callback(() => session = ++order);
sessionContext.Current = new ServiceConfiguration();
sessionContext.Configuration = new ServiceConfiguration();
sut.Terminate();
@ -94,7 +97,7 @@ namespace SafeExamBrowser.Service.UnitTests
bootstrapSequence.Setup(b => b.TryRevert()).Returns(OperationResult.Success).Callback(() => bootstrap = ++order);
sessionSequence.Setup(b => b.TryRevert()).Returns(OperationResult.Success).Callback(() => session = ++order);
sessionContext.Current = null;
sessionContext.Configuration = null;
sut.Terminate();

View file

@ -8,7 +8,9 @@
using System;
using SafeExamBrowser.Communication.Hosts;
using SafeExamBrowser.Contracts.Communication;
using SafeExamBrowser.Contracts.Communication.Data;
using SafeExamBrowser.Contracts.Communication.Events;
using SafeExamBrowser.Contracts.Communication.Hosts;
using SafeExamBrowser.Contracts.Logging;
@ -16,28 +18,68 @@ namespace SafeExamBrowser.Service.Communication
{
internal class ServiceHost : BaseHost, IServiceHost
{
private readonly object @lock = new object();
private bool allowConnection;
public bool AllowConnection
{
get { lock (@lock) { return allowConnection; } }
set { lock (@lock) { allowConnection = value; } }
}
public event CommunicationEventHandler<SessionStartEventArgs> SessionStartRequested;
public event CommunicationEventHandler<SessionStopEventArgs> SessionStopRequested;
internal ServiceHost(string address, IHostObjectFactory factory, ILogger logger, int timeout_ms) : base(address, factory, logger, timeout_ms)
{
AllowConnection = true;
}
protected override bool OnConnect(Guid? token)
{
throw new NotImplementedException();
lock (@lock)
{
var allow = AllowConnection;
if (allow)
{
AllowConnection = false;
}
return allow;
}
}
protected override void OnDisconnect()
protected override void OnDisconnect(Interlocutor interlocutor)
{
throw new NotImplementedException();
if (interlocutor == Interlocutor.Runtime)
{
lock (@lock)
{
AllowConnection = true;
}
}
}
protected override Response OnReceive(Message message)
{
throw new NotImplementedException();
switch (message)
{
case SessionStartMessage m:
SessionStartRequested?.InvokeAsync(new SessionStartEventArgs { Configuration = m.Configuration });
return new SimpleResponse(SimpleResponsePurport.Acknowledged);
case SessionStopMessage m:
SessionStopRequested?.InvokeAsync(new SessionStopEventArgs { SessionId = m.SessionId });
return new SimpleResponse(SimpleResponsePurport.Acknowledged);
}
return new SimpleResponse(SimpleResponsePurport.UnknownMessage);
}
protected override Response OnReceive(SimpleMessagePurport message)
{
throw new NotImplementedException();
return new SimpleResponse(SimpleResponsePurport.UnknownMessage);
}
}
}

View file

@ -10,6 +10,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using SafeExamBrowser.Communication.Hosts;
using SafeExamBrowser.Communication.Proxies;
using SafeExamBrowser.Contracts.Core.OperationModel;
using SafeExamBrowser.Contracts.Logging;
using SafeExamBrowser.Contracts.Service;
@ -17,6 +18,7 @@ using SafeExamBrowser.Core.OperationModel;
using SafeExamBrowser.Core.Operations;
using SafeExamBrowser.Logging;
using SafeExamBrowser.Service.Communication;
using SafeExamBrowser.Service.Operations;
namespace SafeExamBrowser.Service
{
@ -33,25 +35,26 @@ namespace SafeExamBrowser.Service
InitializeLogging();
var proxyFactory = new ProxyFactory(new ProxyObjectFactory(), new ModuleLogger(logger, nameof(ProxyFactory)));
var serviceHost = new ServiceHost(SERVICE_ADDRESS, new HostObjectFactory(), new ModuleLogger(logger, nameof(ServiceHost)), FIVE_SECONDS);
var sessionContext = new SessionContext();
var bootstrapOperations = new Queue<IOperation>();
var sessionOperations = new Queue<IRepeatableOperation>();
var sessionOperations = new Queue<IOperation>();
// TODO: bootstrapOperations.Enqueue(new RestoreOperation());
bootstrapOperations.Enqueue(new CommunicationHostOperation(serviceHost, logger));
bootstrapOperations.Enqueue(new ServiceEventCleanupOperation(logger, sessionContext));
// sessionOperations.Enqueue(new RuntimeConnectionOperation());
// sessionOperations.Enqueue(new LogOperation());
// sessionOperations.Enqueue(new RegistryOperation());
// sessionOperations.Enqueue(new WindowsUpdateOperation());
// sessionOperations.Enqueue(new SessionActivationOperation());
sessionOperations.Enqueue(new SessionInitializationOperation(logger, serviceHost, sessionContext));
// TODO: sessionOperations.Enqueue(new RegistryOperation());
// sessionOperations.Enqueue(new WindowsUpdateOperation());
sessionOperations.Enqueue(new SessionActivationOperation(logger, sessionContext));
var bootstrapSequence = new OperationSequence(logger, bootstrapOperations);
var sessionSequence = new RepeatableOperationSequence(logger, sessionOperations);
var sessionSequence = new OperationSequence(logger, sessionOperations);
ServiceController = new ServiceController(bootstrapSequence, sessionSequence, serviceHost, sessionContext);
ServiceController = new ServiceController(logger, bootstrapSequence, sessionSequence, serviceHost, sessionContext);
}
internal void LogStartupInformation()
@ -62,7 +65,6 @@ namespace SafeExamBrowser.Service
internal void LogShutdownInformation()
{
logger?.Log(string.Empty);
logger?.Log($"# Service terminated at {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
}

View file

@ -0,0 +1,46 @@
/*
* 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 SafeExamBrowser.Contracts.Core.OperationModel;
using SafeExamBrowser.Contracts.Core.OperationModel.Events;
using SafeExamBrowser.Contracts.Logging;
namespace SafeExamBrowser.Service.Operations
{
internal class ServiceEventCleanupOperation : IOperation
{
private ILogger logger;
private SessionContext sessionContext;
public event ActionRequiredEventHandler ActionRequired { add { } remove { } }
public event StatusChangedEventHandler StatusChanged { add { } remove { } }
public ServiceEventCleanupOperation(ILogger logger, SessionContext sessionContext)
{
this.logger = logger;
this.sessionContext = sessionContext;
}
public OperationResult Perform()
{
return OperationResult.Success;
}
public OperationResult Revert()
{
if (sessionContext.EventWaitHandle != null)
{
logger.Info("Closing service event...");
sessionContext.EventWaitHandle.Close();
logger.Info("Service event successfully closed.");
}
return OperationResult.Success;
}
}
}

View file

@ -0,0 +1,44 @@
/*
* 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 SafeExamBrowser.Contracts.Core.OperationModel;
using SafeExamBrowser.Contracts.Logging;
namespace SafeExamBrowser.Service.Operations
{
internal class SessionActivationOperation : SessionOperation
{
private ILogger logger;
public SessionActivationOperation(ILogger logger, SessionContext sessionContext) : base(sessionContext)
{
this.logger = logger;
}
public override OperationResult Perform()
{
var success = Context.EventWaitHandle.Set();
if (success)
{
logger.Info("Successfully informed runtime about new session activation.");
}
else
{
logger.Error("Failed to inform runtime about new session activation!");
}
return success ? OperationResult.Success : OperationResult.Failed;
}
public override OperationResult Revert()
{
return OperationResult.Success;
}
}
}

View file

@ -0,0 +1,84 @@
/*
* 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.Security.AccessControl;
using System.Security.Principal;
using System.Threading;
using SafeExamBrowser.Contracts.Communication.Hosts;
using SafeExamBrowser.Contracts.Core.OperationModel;
using SafeExamBrowser.Contracts.Logging;
namespace SafeExamBrowser.Service.Operations
{
internal class SessionInitializationOperation : SessionOperation
{
private ILogger logger;
private IServiceHost serviceHost;
public SessionInitializationOperation(ILogger logger, IServiceHost serviceHost, SessionContext sessionContext) : base(sessionContext)
{
this.logger = logger;
this.serviceHost = serviceHost;
}
public override OperationResult Perform()
{
logger.Info("Initializing new session...");
serviceHost.AllowConnection = false;
logger.Info($" -> Client-ID: {Context.Configuration.AppConfig.ClientId}");
logger.Info($" -> Runtime-ID: {Context.Configuration.AppConfig.RuntimeId}");
logger.Info($" -> Session-ID: {Context.Configuration.SessionId}");
InitializeEventWaitHandle();
return OperationResult.Success;
}
public override OperationResult Revert()
{
logger.Info("Finalizing current session...");
var success = Context.EventWaitHandle?.Set() == true;
if (success)
{
logger.Info("Successfully informed runtime about session termination.");
}
else
{
logger.Error("Failed to inform runtime about session termination!");
}
Context.Configuration = null;
serviceHost.AllowConnection = true;
return success ? OperationResult.Success : OperationResult.Failed;
}
private void InitializeEventWaitHandle()
{
var securityIdentifier = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null);
var accessRule = new EventWaitHandleAccessRule(securityIdentifier, EventWaitHandleRights.Synchronize, AccessControlType.Allow);
var security = new EventWaitHandleSecurity();
security.AddAccessRule(accessRule);
if (Context.EventWaitHandle != null)
{
logger.Info("Closing service event from previous session...");
Context.EventWaitHandle.Close();
logger.Info("Service event successfully closed.");
}
logger.Info("Attempting to create new service event...");
Context.EventWaitHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Context.Configuration.AppConfig.ServiceEventName, out _, security);
logger.Info("Service event successfully created.");
}
}
}

View file

@ -0,0 +1,32 @@
/*
* 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 SafeExamBrowser.Contracts.Core.OperationModel;
using SafeExamBrowser.Contracts.Core.OperationModel.Events;
namespace SafeExamBrowser.Service.Operations
{
/// <summary>
/// The base implementation to be used for all operations in the session operation sequence.
/// </summary>
internal abstract class SessionOperation : IOperation
{
protected SessionContext Context { get; private set; }
public event ActionRequiredEventHandler ActionRequired { add { } remove { } }
public event StatusChangedEventHandler StatusChanged { add { } remove { } }
public SessionOperation(SessionContext sessionContext)
{
Context = sessionContext;
}
public abstract OperationResult Perform();
public abstract OperationResult Revert();
}
}

View file

@ -64,6 +64,10 @@
<Compile Include="Installer.cs">
<SubType>Component</SubType>
</Compile>
<Compile Include="Operations\ServiceEventCleanupOperation.cs" />
<Compile Include="Operations\SessionActivationOperation.cs" />
<Compile Include="Operations\SessionInitializationOperation.cs" />
<Compile Include="Operations\SessionOperation.cs" />
<Compile Include="Service.cs">
<SubType>Component</SubType>
</Compile>
@ -92,8 +96,6 @@
<Name>SafeExamBrowser.Logging</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Folder Include="Operations\" />
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View file

@ -6,23 +6,27 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using SafeExamBrowser.Contracts.Communication.Events;
using SafeExamBrowser.Contracts.Communication.Hosts;
using SafeExamBrowser.Contracts.Configuration;
using SafeExamBrowser.Contracts.Core.OperationModel;
using SafeExamBrowser.Contracts.Logging;
using SafeExamBrowser.Contracts.Service;
namespace SafeExamBrowser.Service
{
internal class ServiceController : IServiceController
{
private readonly ILogger logger;
private IOperationSequence bootstrapSequence;
private IRepeatableOperationSequence sessionSequence;
private IOperationSequence sessionSequence;
private IServiceHost serviceHost;
private SessionContext sessionContext;
private ServiceConfiguration Session
{
get { return sessionContext.Current; }
get { return sessionContext.Configuration; }
}
private bool SessionIsRunning
@ -31,11 +35,13 @@ namespace SafeExamBrowser.Service
}
public ServiceController(
ILogger logger,
IOperationSequence bootstrapSequence,
IRepeatableOperationSequence sessionSequence,
IOperationSequence sessionSequence,
IServiceHost serviceHost,
SessionContext sessionContext)
{
this.logger = logger;
this.bootstrapSequence = bootstrapSequence;
this.sessionSequence = sessionSequence;
this.serviceHost = serviceHost;
@ -44,22 +50,130 @@ namespace SafeExamBrowser.Service
public bool TryStart()
{
logger.Info("Initiating startup procedure...");
var result = bootstrapSequence.TryPerform();
var success = result == OperationResult.Success;
if (success)
{
RegisterEvents();
logger.Info("Service successfully initialized.");
logger.Log(string.Empty);
}
else
{
logger.Info("Service startup aborted!");
logger.Log(string.Empty);
}
return success;
}
public void Terminate()
{
var result = default(OperationResult);
DeregisterEvents();
if (SessionIsRunning)
{
result = sessionSequence.TryRevert();
StopSession();
}
result = bootstrapSequence.TryRevert();
logger.Log(string.Empty);
logger.Info("Initiating termination procedure...");
var result = bootstrapSequence.TryRevert();
var success = result == OperationResult.Success;
if (success)
{
logger.Info("Service successfully terminated.");
logger.Log(string.Empty);
}
else
{
logger.Info("Service termination failed!");
logger.Log(string.Empty);
}
}
private void StartSession()
{
logger.Info(AppendDivider("Session Start Procedure"));
var result = sessionSequence.TryPerform();
if (result == OperationResult.Success)
{
logger.Info(AppendDivider("Session Running"));
}
else
{
logger.Info(AppendDivider("Session Start Failed"));
}
}
private void StopSession()
{
logger.Info(AppendDivider("Session Stop Procedure"));
var result = sessionSequence.TryRevert();
if (result == OperationResult.Success)
{
logger.Info(AppendDivider("Session Terminated"));
}
else
{
logger.Info(AppendDivider("Session Stop Failed"));
}
}
private void RegisterEvents()
{
serviceHost.SessionStartRequested += ServiceHost_SessionStartRequested;
serviceHost.SessionStopRequested += ServiceHost_SessionStopRequested;
}
private void DeregisterEvents()
{
serviceHost.SessionStartRequested -= ServiceHost_SessionStartRequested;
serviceHost.SessionStopRequested -= ServiceHost_SessionStopRequested;
}
private void ServiceHost_SessionStartRequested(SessionStartEventArgs args)
{
if (!SessionIsRunning)
{
sessionContext.Configuration = args.Configuration;
StartSession();
}
else
{
logger.Warn("Received session start request, even though a session is already running!");
}
}
private void ServiceHost_SessionStopRequested(SessionStopEventArgs args)
{
if (SessionIsRunning)
{
StopSession();
}
else
{
logger.Warn("Received session stop request, even though no session is currently running!");
}
}
private string AppendDivider(string message)
{
var dashesLeft = new String('-', 48 - message.Length / 2 - message.Length % 2);
var dashesRight = new String('-', 48 - message.Length / 2);
return $"### {dashesLeft} {message} {dashesRight} ###";
}
}
}

View file

@ -6,6 +6,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System.Threading;
using SafeExamBrowser.Contracts.Configuration;
namespace SafeExamBrowser.Service
@ -18,11 +19,11 @@ namespace SafeExamBrowser.Service
/// <summary>
/// The configuration of the currently active session.
/// </summary>
internal ServiceConfiguration Current { get; set; }
internal ServiceConfiguration Configuration { get; set; }
/// <summary>
/// The configuration of the next session to be activated.
/// The global inter-process event used for status synchronization with the runtime component.
/// </summary>
internal ServiceConfiguration Next { get; set; }
internal EventWaitHandle EventWaitHandle { get; set; }
}
}

View file

@ -20,14 +20,15 @@ test_script:
- .\packages\OpenCover.4.7.922\tools\OpenCover.Console.exe -register:user -target:"vstest.console.exe" -targetargs:"/logger:Appveyor .\SafeExamBrowser.I18n.UnitTests\bin\%PLATFORM%\%CONFIGURATION%\SafeExamBrowser.I18n.UnitTests.dll" -filter:"+[*]* -[*.UnitTests]* -[*Moq*]*" -mergebyhash -mergeoutput -output:"coverage.xml"
- .\packages\OpenCover.4.7.922\tools\OpenCover.Console.exe -register:user -target:"vstest.console.exe" -targetargs:"/logger:Appveyor .\SafeExamBrowser.Logging.UnitTests\bin\%PLATFORM%\%CONFIGURATION%\SafeExamBrowser.Logging.UnitTests.dll" -filter:"+[*]* -[*.UnitTests]* -[*Moq*]*" -mergebyhash -mergeoutput -output:"coverage.xml"
- .\packages\OpenCover.4.7.922\tools\OpenCover.Console.exe -register:user -target:"vstest.console.exe" -targetargs:"/logger:Appveyor .\SafeExamBrowser.Runtime.UnitTests\bin\%PLATFORM%\%CONFIGURATION%\SafeExamBrowser.Runtime.UnitTests.dll" -filter:"+[*]* -[*.UnitTests]* -[*Moq*]*" -mergebyhash -mergeoutput -output:"coverage.xml"
- .\packages\OpenCover.4.7.922\tools\OpenCover.Console.exe -register:user -target:"vstest.console.exe" -targetargs:"/logger:Appveyor .\SafeExamBrowser.Service.UnitTests\bin\%PLATFORM%\%CONFIGURATION%\SafeExamBrowser.Service.UnitTests.dll" -filter:"+[*]* -[*.UnitTests]* -[*Moq*]*" -mergebyhash -mergeoutput -output:"coverage.xml"
after_test:
- "SET PATH=C:\\Python34;C:\\Python34\\Scripts;%PATH%"
- pip install codecov
- codecov -f "coverage.xml"
artifacts:
- path: SafeExamBrowser.Runtime\bin\$(platform)\$(configuration)
name: '$(appveyor_build_version)_Base_Application'
name: '$(appveyor_build_version)_Application'
type: zip
- path: SafeExamBrowser.Service\bin\$(platform)\$(configuration)
name: '$(appveyor_build_version)_Base_Service'
name: '$(appveyor_build_version)_Service'
type: zip