diff --git a/SafeExamBrowser.Browser/BrowserApplicationController.cs b/SafeExamBrowser.Browser/BrowserApplicationController.cs
index c7a6884e..0b39dc75 100644
--- a/SafeExamBrowser.Browser/BrowserApplicationController.cs
+++ b/SafeExamBrowser.Browser/BrowserApplicationController.cs
@@ -127,7 +127,7 @@ namespace SafeExamBrowser.Browser
var cefSettings = new CefSettings
{
CachePath = appConfig.BrowserCachePath,
- LogFile = appConfig.BrowserLogFile,
+ LogFile = appConfig.BrowserLogFilePath,
LogSeverity = error ? LogSeverity.Error : (warning ? LogSeverity.Warning : LogSeverity.Info),
UserAgent = InitializeUserAgent()
};
diff --git a/SafeExamBrowser.Communication/Proxies/ServiceProxy.cs b/SafeExamBrowser.Communication/Proxies/ServiceProxy.cs
index a665ba68..bdc8c432 100644
--- a/SafeExamBrowser.Communication/Proxies/ServiceProxy.cs
+++ b/SafeExamBrowser.Communication/Proxies/ServiceProxy.cs
@@ -19,6 +19,7 @@ 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)
{
diff --git a/SafeExamBrowser.Configuration.UnitTests/ConfigurationRepositoryTests.cs b/SafeExamBrowser.Configuration.UnitTests/ConfigurationRepositoryTests.cs
index 6e5a483d..b951b151 100644
--- a/SafeExamBrowser.Configuration.UnitTests/ConfigurationRepositoryTests.cs
+++ b/SafeExamBrowser.Configuration.UnitTests/ConfigurationRepositoryTests.cs
@@ -287,18 +287,18 @@ namespace SafeExamBrowser.Configuration.UnitTests
var appConfig = sut.InitializeAppConfig();
var clientAddress = appConfig.ClientAddress;
var clientId = appConfig.ClientId;
- var clientLogFile = appConfig.ClientLogFile;
+ var clientLogFilePath = appConfig.ClientLogFilePath;
var runtimeAddress = appConfig.RuntimeAddress;
var runtimeId = appConfig.RuntimeId;
- var runtimeLogFile = appConfig.RuntimeLogFile;
+ var runtimeLogFilePath = appConfig.RuntimeLogFilePath;
var configuration = sut.InitializeSessionConfiguration();
Assert.AreNotEqual(configuration.AppConfig.ClientAddress, clientAddress);
Assert.AreNotEqual(configuration.AppConfig.ClientId, clientId);
- Assert.AreEqual(configuration.AppConfig.ClientLogFile, clientLogFile);
+ Assert.AreEqual(configuration.AppConfig.ClientLogFilePath, clientLogFilePath);
Assert.AreEqual(configuration.AppConfig.RuntimeAddress, runtimeAddress);
Assert.AreEqual(configuration.AppConfig.RuntimeId, runtimeId);
- Assert.AreEqual(configuration.AppConfig.RuntimeLogFile, runtimeLogFile);
+ Assert.AreEqual(configuration.AppConfig.RuntimeLogFilePath, runtimeLogFilePath);
}
[TestMethod]
diff --git a/SafeExamBrowser.Configuration/ConfigurationData/DataValues.cs b/SafeExamBrowser.Configuration/ConfigurationData/DataValues.cs
index 6d1106eb..4ba33965 100644
--- a/SafeExamBrowser.Configuration/ConfigurationData/DataValues.cs
+++ b/SafeExamBrowser.Configuration/ConfigurationData/DataValues.cs
@@ -17,6 +17,7 @@ namespace SafeExamBrowser.Configuration.ConfigurationData
internal class DataValues
{
private const string BASE_ADDRESS = "net.pipe://localhost/safeexambrowser";
+ private const string DEFAULT_FILE_NAME = "SebClientSettings.seb";
private AppConfig appConfig;
private string executablePath;
@@ -34,35 +35,36 @@ namespace SafeExamBrowser.Configuration.ConfigurationData
internal string GetAppDataFilePath()
{
- return Path.Combine(appConfig.AppDataFolder, appConfig.DefaultSettingsFileName);
+ return appConfig.AppDataFilePath;
}
internal AppConfig InitializeAppConfig()
{
- var appDataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), nameof(SafeExamBrowser));
+ var appDataLocalFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), nameof(SafeExamBrowser));
+ var appDataRoamingFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), nameof(SafeExamBrowser));
+ var programDataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), nameof(SafeExamBrowser));
var startTime = DateTime.Now;
- var logFolder = Path.Combine(appDataFolder, "Logs");
+ var logFolder = Path.Combine(appDataLocalFolder, "Logs");
var logFilePrefix = startTime.ToString("yyyy-MM-dd\\_HH\\hmm\\mss\\s");
appConfig = new AppConfig();
+ appConfig.AppDataFilePath = Path.Combine(appDataRoamingFolder, DEFAULT_FILE_NAME);
appConfig.ApplicationStartTime = startTime;
- appConfig.AppDataFolder = appDataFolder;
- appConfig.BrowserCachePath = Path.Combine(appDataFolder, "Cache");
- appConfig.BrowserLogFile = Path.Combine(logFolder, $"{logFilePrefix}_Browser.log");
+ appConfig.BrowserCachePath = Path.Combine(appDataLocalFolder, "Cache");
+ appConfig.BrowserLogFilePath = Path.Combine(logFolder, $"{logFilePrefix}_Browser.log");
appConfig.ClientId = Guid.NewGuid();
appConfig.ClientAddress = $"{BASE_ADDRESS}/client/{Guid.NewGuid()}";
appConfig.ClientExecutablePath = Path.Combine(Path.GetDirectoryName(executablePath), $"{nameof(SafeExamBrowser)}.Client.exe");
- appConfig.ClientLogFile = Path.Combine(logFolder, $"{logFilePrefix}_Client.log");
+ appConfig.ClientLogFilePath = Path.Combine(logFolder, $"{logFilePrefix}_Client.log");
appConfig.ConfigurationFileExtension = ".seb";
- appConfig.DefaultSettingsFileName = "SebClientSettings.seb";
- appConfig.DownloadDirectory = Path.Combine(appDataFolder, "Downloads");
+ appConfig.DownloadDirectory = Path.Combine(appDataLocalFolder, "Downloads");
appConfig.ProgramCopyright = programCopyright;
- appConfig.ProgramDataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), nameof(SafeExamBrowser));
+ appConfig.ProgramDataFilePath = Path.Combine(programDataFolder, DEFAULT_FILE_NAME);
appConfig.ProgramTitle = programTitle;
appConfig.ProgramVersion = programVersion;
appConfig.RuntimeId = Guid.NewGuid();
appConfig.RuntimeAddress = $"{BASE_ADDRESS}/runtime/{Guid.NewGuid()}";
- appConfig.RuntimeLogFile = Path.Combine(logFolder, $"{logFilePrefix}_Runtime.log");
+ appConfig.RuntimeLogFilePath = Path.Combine(logFolder, $"{logFilePrefix}_Runtime.log");
appConfig.SebUriScheme = "seb";
appConfig.SebUriSchemeSecure = "sebs";
appConfig.ServiceAddress = $"{BASE_ADDRESS}/service";
diff --git a/SafeExamBrowser.Contracts/Communication/Hosts/IRuntimeHost.cs b/SafeExamBrowser.Contracts/Communication/Hosts/IRuntimeHost.cs
index 3c875e74..261d76a6 100644
--- a/SafeExamBrowser.Contracts/Communication/Hosts/IRuntimeHost.cs
+++ b/SafeExamBrowser.Contracts/Communication/Hosts/IRuntimeHost.cs
@@ -32,7 +32,7 @@ namespace SafeExamBrowser.Contracts.Communication.Hosts
event CommunicationEventHandler ClientDisconnected;
///
- /// Event fired once the client has signaled that it is ready to operate.
+ /// Event fired when the client has signaled that it is ready to operate.
///
event CommunicationEventHandler ClientReady;
@@ -56,6 +56,26 @@ namespace SafeExamBrowser.Contracts.Communication.Hosts
///
event CommunicationEventHandler ReconfigurationRequested;
+ ///
+ /// Event fired when the service disconnected from the runtime.
+ ///
+ event CommunicationEventHandler ServiceDisconnected;
+
+ ///
+ /// Event fired when the service has experienced a critical failure.
+ ///
+ event CommunicationEventHandler ServiceFailed;
+
+ ///
+ /// Event fired when the service has successfully started a new session.
+ ///
+ event CommunicationEventHandler ServiceSessionStarted;
+
+ ///
+ /// Event fired when the service has successfully stopped the currently running session.
+ ///
+ event CommunicationEventHandler ServiceSessionStopped;
+
///
/// Event fired when the client requests to shut down the application.
///
diff --git a/SafeExamBrowser.Contracts/Communication/Proxies/IServiceProxy.cs b/SafeExamBrowser.Contracts/Communication/Proxies/IServiceProxy.cs
index ef626c80..b4e85d80 100644
--- a/SafeExamBrowser.Contracts/Communication/Proxies/IServiceProxy.cs
+++ b/SafeExamBrowser.Contracts/Communication/Proxies/IServiceProxy.cs
@@ -22,6 +22,11 @@ namespace SafeExamBrowser.Contracts.Communication.Proxies
///
bool Ignore { set; }
+ ///
+ /// Indicates whether a connection to the communication host of the service has been established.
+ ///
+ bool IsConnected { get; }
+
///
/// Instructs the service to start a new session according to the given parameters.
///
diff --git a/SafeExamBrowser.Contracts/Configuration/AppConfig.cs b/SafeExamBrowser.Contracts/Configuration/AppConfig.cs
index 5ca634db..07002a37 100644
--- a/SafeExamBrowser.Contracts/Configuration/AppConfig.cs
+++ b/SafeExamBrowser.Contracts/Configuration/AppConfig.cs
@@ -17,9 +17,9 @@ namespace SafeExamBrowser.Contracts.Configuration
public class AppConfig
{
///
- /// The path of the application data folder.
+ /// The file path of the local client configuration for the active user.
///
- public string AppDataFolder { get; set; }
+ public string AppDataFilePath { get; set; }
///
/// The point in time when the application was started.
@@ -34,7 +34,7 @@ namespace SafeExamBrowser.Contracts.Configuration
///
/// The file path under which the log of the browser component is to be stored.
///
- public string BrowserLogFile { get; set; }
+ public string BrowserLogFilePath { get; set; }
///
/// The communication address of the client component.
@@ -54,18 +54,13 @@ namespace SafeExamBrowser.Contracts.Configuration
///
/// The file path under which the log of the client component is to be stored.
///
- public string ClientLogFile { get; set; }
+ public string ClientLogFilePath { get; set; }
///
/// The file extension of configuration files for the application (including the period).
///
public string ConfigurationFileExtension { get; set; }
- ///
- /// The default file name for application settings.
- ///
- public string DefaultSettingsFileName { get; set; }
-
///
/// The default directory for file downloads.
///
@@ -77,9 +72,9 @@ namespace SafeExamBrowser.Contracts.Configuration
public string ProgramCopyright { get; set; }
///
- /// The path of the program data folder.
+ /// The file path of the local client configuration for all users.
///
- public string ProgramDataFolder { get; set; }
+ public string ProgramDataFilePath { get; set; }
///
/// The program title of the application (i.e. the executing assembly).
@@ -104,7 +99,7 @@ namespace SafeExamBrowser.Contracts.Configuration
///
/// The file path under which the log of the runtime component is to be stored.
///
- public string RuntimeLogFile { get; set; }
+ public string RuntimeLogFilePath { get; set; }
///
/// The URI scheme for SEB resources.
diff --git a/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj b/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj
index 9f38342d..88d2ab3a 100644
--- a/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj
+++ b/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj
@@ -144,6 +144,7 @@
+
diff --git a/SafeExamBrowser.Contracts/Service/IServiceController.cs b/SafeExamBrowser.Contracts/Service/IServiceController.cs
new file mode 100644
index 00000000..a5186e39
--- /dev/null
+++ b/SafeExamBrowser.Contracts/Service/IServiceController.cs
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+namespace SafeExamBrowser.Contracts.Service
+{
+ ///
+ /// Controls the lifetime and is responsible for the event handling of the service application component.
+ ///
+ public interface IServiceController
+ {
+ ///
+ /// Reverts any changes and releases all resources used by the service.
+ ///
+ void Terminate();
+
+ ///
+ /// Tries to start the service. Returns true if successful, otherwise false.
+ ///
+ bool TryStart();
+ }
+}
diff --git a/SafeExamBrowser.Runtime.UnitTests/Operations/ConfigurationOperationTests.cs b/SafeExamBrowser.Runtime.UnitTests/Operations/ConfigurationOperationTests.cs
index 1d0e547c..f139e73e 100644
--- a/SafeExamBrowser.Runtime.UnitTests/Operations/ConfigurationOperationTests.cs
+++ b/SafeExamBrowser.Runtime.UnitTests/Operations/ConfigurationOperationTests.cs
@@ -24,6 +24,8 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
[TestClass]
public class ConfigurationOperationTests
{
+ private const string FILE_NAME = "SebClientSettings.seb";
+
private AppConfig appConfig;
private Mock hashAlgorithm;
private Mock logger;
@@ -43,9 +45,8 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
nextSession = new Mock();
sessionContext = new SessionContext();
- appConfig.AppDataFolder = @"C:\Not\Really\AppData";
- appConfig.DefaultSettingsFileName = "SettingsDummy.txt";
- appConfig.ProgramDataFolder = @"C:\Not\Really\ProgramData";
+ appConfig.AppDataFilePath = $@"C:\Not\Really\AppData\File.xml";
+ appConfig.ProgramDataFilePath = $@"C:\Not\Really\ProgramData\File.xml";
currentSession.SetupGet(s => s.AppConfig).Returns(appConfig);
nextSession.SetupGet(s => s.AppConfig).Returns(appConfig);
sessionContext.Current = currentSession.Object;
@@ -57,11 +58,10 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
{
var settings = new Settings { ConfigurationMode = ConfigurationMode.Exam };
var url = @"http://www.safeexambrowser.org/whatever.seb";
- var location = Path.Combine(Path.GetDirectoryName(GetType().Assembly.Location), nameof(Operations));
+ var location = Path.Combine(Path.GetDirectoryName(GetType().Assembly.Location), nameof(Operations), FILE_NAME);
- appConfig.AppDataFolder = location;
- appConfig.ProgramDataFolder = location;
- appConfig.DefaultSettingsFileName = "SettingsDummy.txt";
+ appConfig.AppDataFilePath = location;
+ appConfig.ProgramDataFilePath = location;
repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, It.IsAny())).Returns(LoadStatus.Success);
@@ -76,36 +76,33 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
[TestMethod]
public void Perform_MustUseProgramDataAs2ndPrio()
{
- var location = Path.Combine(Path.GetDirectoryName(GetType().Assembly.Location), nameof(Operations));
+ var location = Path.Combine(Path.GetDirectoryName(GetType().Assembly.Location), nameof(Operations), FILE_NAME);
var settings = default(Settings);
- appConfig.ProgramDataFolder = location;
- appConfig.AppDataFolder = $@"{location}\WRONG";
+ appConfig.ProgramDataFilePath = location;
repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, It.IsAny())).Returns(LoadStatus.Success);
var sut = new ConfigurationOperation(null, repository.Object, hashAlgorithm.Object, logger.Object, sessionContext);
var result = sut.Perform();
- var resource = new Uri(Path.Combine(location, "SettingsDummy.txt"));
- repository.Verify(r => r.TryLoadSettings(It.Is(u => u.Equals(resource)), out settings, It.IsAny()), Times.Once);
+ repository.Verify(r => r.TryLoadSettings(It.Is(u => u.Equals(location)), out settings, It.IsAny()), Times.Once);
Assert.AreEqual(OperationResult.Success, result);
}
[TestMethod]
public void Perform_MustUseAppDataAs3rdPrio()
{
- var location = Path.Combine(Path.GetDirectoryName(GetType().Assembly.Location), nameof(Operations));
+ var location = Path.Combine(Path.GetDirectoryName(GetType().Assembly.Location), nameof(Operations), FILE_NAME);
var settings = default(Settings);
- appConfig.AppDataFolder = location;
+ appConfig.AppDataFilePath = location;
repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, It.IsAny())).Returns(LoadStatus.Success);
var sut = new ConfigurationOperation(null, repository.Object, hashAlgorithm.Object, logger.Object, sessionContext);
var result = sut.Perform();
- var resource = new Uri(Path.Combine(location, "SettingsDummy.txt"));
- repository.Verify(r => r.TryLoadSettings(It.Is(u => u.Equals(resource)), out settings, It.IsAny()), Times.Once);
+ repository.Verify(r => r.TryLoadSettings(It.Is(u => u.Equals(location)), out settings, It.IsAny()), Times.Once);
Assert.AreEqual(OperationResult.Success, result);
}
@@ -284,10 +281,10 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
var settings = new Settings { AdminPasswordHash = "9876", ConfigurationMode = ConfigurationMode.ConfigureClient };
var url = @"http://www.safeexambrowser.org/whatever.seb";
- appConfig.AppDataFolder = Path.Combine(Path.GetDirectoryName(GetType().Assembly.Location), nameof(Operations));
+ appConfig.AppDataFilePath = Path.Combine(Path.GetDirectoryName(GetType().Assembly.Location), nameof(Operations), FILE_NAME);
repository.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, It.IsAny())).Returns(LoadStatus.Success);
- repository.Setup(r => r.TryLoadSettings(It.Is(u => u.LocalPath.Contains("SettingsDummy")), out localSettings, It.IsAny())).Returns(LoadStatus.Success);
+ repository.Setup(r => r.TryLoadSettings(It.Is(u => u.LocalPath.Contains(FILE_NAME)), out localSettings, It.IsAny())).Returns(LoadStatus.Success);
nextSession.SetupGet(s => s.Settings).Returns(settings);
var sut = new ConfigurationOperation(new[] { "blubb.exe", url }, repository.Object, hashAlgorithm.Object, logger.Object, sessionContext);
@@ -341,7 +338,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
var nextSettings = new Settings { AdminPasswordHash = "9876", ConfigurationMode = ConfigurationMode.ConfigureClient };
var url = @"http://www.safeexambrowser.org/whatever.seb";
- appConfig.AppDataFolder = Path.Combine(Path.GetDirectoryName(GetType().Assembly.Location), nameof(Operations));
+ appConfig.AppDataFilePath = Path.Combine(Path.GetDirectoryName(GetType().Assembly.Location), nameof(Operations), FILE_NAME);
nextSession.SetupGet(s => s.Settings).Returns(nextSettings);
hashAlgorithm.Setup(h => h.GenerateHashFor(It.Is(p => p == password))).Returns(currentSettings.AdminPasswordHash);
@@ -373,7 +370,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
var nextSettings = new Settings { AdminPasswordHash = "1234", ConfigurationMode = ConfigurationMode.ConfigureClient };
var url = @"http://www.safeexambrowser.org/whatever.seb";
- appConfig.AppDataFolder = Path.Combine(Path.GetDirectoryName(GetType().Assembly.Location), nameof(Operations));
+ appConfig.AppDataFilePath = Path.Combine(Path.GetDirectoryName(GetType().Assembly.Location), nameof(Operations), FILE_NAME);
nextSession.SetupGet(s => s.Settings).Returns(nextSettings);
repository.Setup(r => r.TryLoadSettings(It.IsAny(), out currentSettings, It.IsAny())).Returns(LoadStatus.Success);
@@ -427,18 +424,16 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
public void Perform_MustUseCurrentPasswordIfAvailable()
{
var url = @"http://www.safeexambrowser.org/whatever.seb";
- var location = Path.Combine(Path.GetDirectoryName(GetType().Assembly.Location), nameof(Operations));
- var appDataFile = new Uri(Path.Combine(location, "SettingsDummy.txt"));
+ var location = Path.Combine(Path.GetDirectoryName(GetType().Assembly.Location), nameof(Operations), FILE_NAME);
var settings = new Settings { AdminPasswordHash = "1234", ConfigurationMode = ConfigurationMode.Exam };
- appConfig.AppDataFolder = location;
- appConfig.DefaultSettingsFileName = "SettingsDummy.txt";
+ appConfig.AppDataFilePath = location;
repository
.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, It.IsAny()))
.Returns(LoadStatus.PasswordNeeded);
repository
- .Setup(r => r.TryLoadSettings(It.Is(u => u.Equals(appDataFile)), out settings, It.IsAny()))
+ .Setup(r => r.TryLoadSettings(It.Is(u => u.Equals(new Uri(location))), out settings, It.IsAny()))
.Returns(LoadStatus.Success);
repository
.Setup(r => r.TryLoadSettings(It.IsAny(), out settings, It.Is(p => p.IsHash == true && p.Password == settings.AdminPasswordHash)))
@@ -460,7 +455,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
var nextSettings = new Settings { AdminPasswordHash = "9876", ConfigurationMode = ConfigurationMode.ConfigureClient };
var url = @"http://www.safeexambrowser.org/whatever.seb";
- appConfig.AppDataFolder = Path.Combine(Path.GetDirectoryName(GetType().Assembly.Location), nameof(Operations));
+ appConfig.AppDataFilePath = Path.Combine(Path.GetDirectoryName(GetType().Assembly.Location), nameof(Operations), FILE_NAME);
nextSession.SetupGet(s => s.Settings).Returns(nextSettings);
hashAlgorithm.Setup(h => h.GenerateHashFor(It.Is(p => p == password))).Returns(currentSettings.AdminPasswordHash);
@@ -510,7 +505,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
{
var currentSettings = new Settings();
var location = Path.GetDirectoryName(GetType().Assembly.Location);
- var resource = new Uri(Path.Combine(location, nameof(Operations), "SettingsDummy.txt"));
+ var resource = new Uri(Path.Combine(location, nameof(Operations), FILE_NAME));
var settings = new Settings { ConfigurationMode = ConfigurationMode.Exam };
currentSession.SetupGet(s => s.Settings).Returns(currentSettings);
@@ -532,7 +527,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
{
var currentSettings = new Settings();
var location = Path.GetDirectoryName(GetType().Assembly.Location);
- var resource = new Uri(Path.Combine(location, nameof(Operations), "SettingsDummy.txt"));
+ var resource = new Uri(Path.Combine(location, nameof(Operations), FILE_NAME));
var settings = new Settings { ConfigurationMode = ConfigurationMode.ConfigureClient };
currentSession.SetupGet(s => s.Settings).Returns(currentSettings);
@@ -577,7 +572,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
{
var currentSettings = new Settings();
var location = Path.GetDirectoryName(GetType().Assembly.Location);
- var resource = new Uri(Path.Combine(location, nameof(Operations), "SettingsDummy.txt"));
+ var resource = new Uri(Path.Combine(location, nameof(Operations), FILE_NAME));
var settings = new Settings { ConfigurationMode = ConfigurationMode.ConfigureClient };
currentSession.SetupGet(s => s.Settings).Returns(currentSettings);
diff --git a/SafeExamBrowser.Runtime.UnitTests/Operations/SettingsDummy.txt b/SafeExamBrowser.Runtime.UnitTests/Operations/SebClientSettings.seb
similarity index 100%
rename from SafeExamBrowser.Runtime.UnitTests/Operations/SettingsDummy.txt
rename to SafeExamBrowser.Runtime.UnitTests/Operations/SebClientSettings.seb
diff --git a/SafeExamBrowser.Runtime.UnitTests/Operations/ServiceOperationTests.cs b/SafeExamBrowser.Runtime.UnitTests/Operations/ServiceOperationTests.cs
index a0c56742..4be304bb 100644
--- a/SafeExamBrowser.Runtime.UnitTests/Operations/ServiceOperationTests.cs
+++ b/SafeExamBrowser.Runtime.UnitTests/Operations/ServiceOperationTests.cs
@@ -9,6 +9,7 @@
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
+using SafeExamBrowser.Contracts.Communication.Hosts;
using SafeExamBrowser.Contracts.Communication.Proxies;
using SafeExamBrowser.Contracts.Configuration;
using SafeExamBrowser.Contracts.Configuration.Settings;
@@ -22,6 +23,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
public class ServiceOperationTests
{
private Mock logger;
+ private Mock runtimeHost;
private Mock service;
private Mock session;
private SessionContext sessionContext;
@@ -32,6 +34,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
public void Initialize()
{
logger = new Mock();
+ runtimeHost = new Mock();
service = new Mock();
session = new Mock();
sessionContext = new SessionContext();
@@ -40,12 +43,13 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
sessionContext.Current = session.Object;
sessionContext.Next = session.Object;
session.SetupGet(s => s.Settings).Returns(settings);
+ settings.ServicePolicy = ServicePolicy.Mandatory;
- sut = new ServiceOperation(logger.Object, service.Object, sessionContext);
+ sut = new ServiceOperation(logger.Object, runtimeHost.Object, service.Object, sessionContext, 0);
}
[TestMethod]
- public void MustConnectToService()
+ public void Perform_MustConnectToService()
{
service.Setup(s => s.Connect(null, true)).Returns(true);
settings.ServicePolicy = ServicePolicy.Mandatory;
@@ -61,18 +65,67 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
}
[TestMethod]
- public void MustStartSessionIfConnected()
+ public void Perform_MustStartSessionIfConnected()
{
+ service.SetupGet(s => s.IsConnected).Returns(true);
service.Setup(s => s.Connect(null, true)).Returns(true);
+ service
+ .Setup(s => s.StartSession(It.IsAny(), It.IsAny()))
+ .Returns(new CommunicationResult(true))
+ .Callback(() => runtimeHost.Raise(h => h.ServiceSessionStarted += null));
- sut.Perform();
+ var result = sut.Perform();
service.Verify(s => s.StartSession(It.IsAny(), It.IsAny()), Times.Once);
+
+ Assert.AreEqual(OperationResult.Success, result);
}
[TestMethod]
- public void MustNotStartSessionIfNotConnected()
+ public void Perform_MustFailIfSessionStartUnsuccessful()
{
+ service.SetupGet(s => s.IsConnected).Returns(true);
+ service.Setup(s => s.Connect(null, true)).Returns(true);
+ service
+ .Setup(s => s.StartSession(It.IsAny(), It.IsAny()))
+ .Returns(new CommunicationResult(true))
+ .Callback(() => runtimeHost.Raise(h => h.ServiceFailed += null));
+
+ var result = sut.Perform();
+
+ service.Verify(s => s.StartSession(It.IsAny(), It.IsAny()), Times.Once);
+
+ Assert.AreEqual(OperationResult.Failed, result);
+ }
+
+ [TestMethod]
+ public void Perform_MustFailIfSessionNotStartedWithinTimeout()
+ {
+ const int TIMEOUT = 50;
+
+ var after = default(DateTime);
+ var before = default(DateTime);
+
+ service.SetupGet(s => s.IsConnected).Returns(true);
+ service.Setup(s => s.Connect(null, true)).Returns(true);
+ service.Setup(s => s.StartSession(It.IsAny(), It.IsAny())).Returns(new CommunicationResult(true));
+
+ sut = new ServiceOperation(logger.Object, runtimeHost.Object, service.Object, sessionContext, TIMEOUT);
+
+ before = DateTime.Now;
+ var result = sut.Perform();
+ after = DateTime.Now;
+
+ service.Verify(s => s.StartSession(It.IsAny(), It.IsAny()), Times.Once);
+
+ Assert.AreEqual(OperationResult.Failed, result);
+ Assert.IsTrue(after - before >= new TimeSpan(0, 0, 0, 0, TIMEOUT));
+ }
+
+ [TestMethod]
+ public void Perform_MustNotStartSessionIfNotConnected()
+ {
+ service.SetupGet(s => s.IsConnected).Returns(false);
service.Setup(s => s.Connect(null, true)).Returns(false);
sut.Perform();
@@ -81,22 +134,9 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
}
[TestMethod]
- public void MustNotFailIfServiceNotAvailable()
- {
- service.Setup(s => s.Connect(null, true)).Returns(false);
- settings.ServicePolicy = ServicePolicy.Mandatory;
-
- sut.Perform();
-
- service.Setup(s => s.Connect(null, true)).Returns(false);
- settings.ServicePolicy = ServicePolicy.Optional;
-
- sut.Perform();
- }
-
- [TestMethod]
- public void MustFailIfServiceMandatoryAndNotAvailable()
+ public void Perform_MustFailIfServiceMandatoryAndNotAvailable()
{
+ service.SetupGet(s => s.IsConnected).Returns(false);
service.Setup(s => s.Connect(null, true)).Returns(false);
settings.ServicePolicy = ServicePolicy.Mandatory;
@@ -106,87 +146,185 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
}
[TestMethod]
- public void MustNotFailIfServiceOptionalAndNotAvailable()
+ public void Perform_MustNotFailIfServiceOptionalAndNotAvailable()
{
+ service.SetupGet(s => s.IsConnected).Returns(false);
service.Setup(s => s.Connect(null, true)).Returns(false);
settings.ServicePolicy = ServicePolicy.Optional;
var result = sut.Perform();
service.VerifySet(s => s.Ignore = true);
+ Assert.AreEqual(OperationResult.Success, result);
+ }
+
+ [TestMethod]
+ public void Repeat_MustStopCurrentAndStartNewSession()
+ {
+ service
+ .Setup(s => s.StopSession(It.IsAny()))
+ .Returns(new CommunicationResult(true))
+ .Callback(() => runtimeHost.Raise(h => h.ServiceSessionStopped += null));
+
+ PerformNormally();
+
+ var result = sut.Repeat();
+
+ service.Verify(s => s.Connect(It.IsAny(), It.IsAny()), Times.Once);
+ service.Verify(s => s.StopSession(It.IsAny()), Times.Once);
+ service.Verify(s => s.StartSession(It.IsAny(), It.IsAny()), Times.Exactly(2));
+ service.Verify(s => s.Disconnect(), Times.Never);
Assert.AreEqual(OperationResult.Success, result);
}
[TestMethod]
- public void MustDisconnectWhenReverting()
+ public void Repeat_MustFailIfCurrentSessionWasNotStoppedSuccessfully()
{
- service.Setup(s => s.Connect(null, true)).Returns(true);
- settings.ServicePolicy = ServicePolicy.Mandatory;
+ service.Setup(s => s.StopSession(It.IsAny())).Returns(new CommunicationResult(false));
- sut.Perform();
- sut.Revert();
+ PerformNormally();
- service.Setup(s => s.Connect(null, true)).Returns(true);
- settings.ServicePolicy = ServicePolicy.Optional;
-
- sut.Perform();
- sut.Revert();
-
- service.Verify(s => s.Disconnect(), Times.Exactly(2));
- }
-
- [TestMethod]
- public void MustStopSessionWhenReverting()
- {
- service.Setup(s => s.Connect(null, true)).Returns(true);
-
- sut.Perform();
- sut.Revert();
+ var result = sut.Repeat();
service.Verify(s => s.StopSession(It.IsAny()), Times.Once);
+ service.Verify(s => s.StartSession(It.IsAny(), It.IsAny()), Times.Once);
+ service.Verify(s => s.Disconnect(), Times.Never);
+
+ Assert.AreEqual(OperationResult.Failed, result);
}
[TestMethod]
- public void MustNotStopSessionWhenRevertingAndNotConnected()
+ public void Revert_MustDisconnect()
{
- service.Setup(s => s.Connect(null, true)).Returns(false);
+ service.Setup(s => s.Disconnect()).Returns(true).Callback(() => runtimeHost.Raise(h => h.ServiceDisconnected += null));
+ service
+ .Setup(s => s.StopSession(It.IsAny()))
+ .Returns(new CommunicationResult(true))
+ .Callback(() => runtimeHost.Raise(h => h.ServiceSessionStopped += null));
- sut.Perform();
- sut.Revert();
+ PerformNormally();
- service.Verify(s => s.StopSession(It.IsAny()), Times.Never);
- }
-
- [TestMethod]
- public void MustNotFailWhenDisconnecting()
- {
- service.Setup(s => s.Connect(null, true)).Returns(true);
- service.Setup(s => s.Disconnect()).Returns(false);
- settings.ServicePolicy = ServicePolicy.Optional;
-
- sut.Perform();
- sut.Revert();
+ var result = sut.Revert();
service.Verify(s => s.Disconnect(), Times.Once);
+ Assert.AreEqual(OperationResult.Success, result);
}
[TestMethod]
- public void MustNotDisconnnectIfNotAvailable()
+ public void Revert_MustFailIfServiceNotDisconnectedWithinTimeout()
{
- service.Setup(s => s.Connect(null, true)).Returns(false);
- settings.ServicePolicy = ServicePolicy.Mandatory;
+ const int TIMEOUT = 50;
- sut.Perform();
- sut.Revert();
+ var after = default(DateTime);
+ var before = default(DateTime);
- service.Setup(s => s.Connect(null, true)).Returns(false);
- settings.ServicePolicy = ServicePolicy.Optional;
+ sut = new ServiceOperation(logger.Object, runtimeHost.Object, service.Object, sessionContext, TIMEOUT);
- sut.Perform();
- sut.Revert();
+ service.Setup(s => s.Disconnect()).Returns(true);
+ service
+ .Setup(s => s.StopSession(It.IsAny()))
+ .Returns(new CommunicationResult(true))
+ .Callback(() => runtimeHost.Raise(h => h.ServiceSessionStopped += null));
+
+ PerformNormally();
+
+ before = DateTime.Now;
+ var result = sut.Revert();
+ after = DateTime.Now;
+
+ service.Verify(s => s.Disconnect(), Times.Once);
+
+ Assert.AreEqual(OperationResult.Failed, result);
+ Assert.IsTrue(after - before >= new TimeSpan(0, 0, 0, 0, TIMEOUT));
+ }
+
+ [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()))
+ .Returns(new CommunicationResult(true))
+ .Callback(() => runtimeHost.Raise(h => h.ServiceSessionStopped += null));
+
+ PerformNormally();
+
+ var result = sut.Revert();
+
+ service.Verify(s => s.StopSession(It.IsAny()), Times.Once);
+ service.Verify(s => s.Disconnect(), Times.Once);
+
+ Assert.AreEqual(OperationResult.Success, result);
+ }
+
+ [TestMethod]
+ public void Revert_MustHandleCommunicationFailureWhenStoppingSession()
+ {
+ service.Setup(s => s.StopSession(It.IsAny())).Returns(new CommunicationResult(false));
+
+ PerformNormally();
+
+ var result = sut.Revert();
+
+ service.Verify(s => s.StopSession(It.IsAny()), Times.Once);
+ service.Verify(s => s.Disconnect(), Times.Once);
+
+ Assert.AreEqual(OperationResult.Failed, result);
+ }
+
+ [TestMethod]
+ public void Revert_MustFailIfSessionNotStoppedWithinTimeout()
+ {
+ const int TIMEOUT = 50;
+
+ var after = default(DateTime);
+ var before = default(DateTime);
+
+ service.Setup(s => s.StopSession(It.IsAny())).Returns(new CommunicationResult(true));
+ sut = new ServiceOperation(logger.Object, runtimeHost.Object, service.Object, sessionContext, TIMEOUT);
+
+ PerformNormally();
+
+ before = DateTime.Now;
+ var result = sut.Revert();
+ after = DateTime.Now;
+
+ service.Verify(s => s.StopSession(It.IsAny()), Times.Once);
+ service.Verify(s => s.Disconnect(), Times.Once);
+
+ Assert.AreEqual(OperationResult.Failed, result);
+ Assert.IsTrue(after - before >= new TimeSpan(0, 0, 0, 0, TIMEOUT));
+ }
+
+ [TestMethod]
+ public void Revert_MustNotStopSessionWhenNotConnected()
+ {
+ var result = sut.Revert();
+
+ service.Verify(s => s.StopSession(It.IsAny()), Times.Never);
+ Assert.AreEqual(OperationResult.Success, result);
+ }
+
+ [TestMethod]
+ public void Revert_MustNotDisconnnectIfNotConnected()
+ {
+ var result = sut.Revert();
service.Verify(s => s.Disconnect(), Times.Never);
+ Assert.AreEqual(OperationResult.Success, result);
+ }
+
+ private void PerformNormally()
+ {
+ service.SetupGet(s => s.IsConnected).Returns(true);
+ service.Setup(s => s.Connect(null, true)).Returns(true);
+ service
+ .Setup(s => s.StartSession(It.IsAny(), It.IsAny()))
+ .Returns(new CommunicationResult(true))
+ .Callback(() => runtimeHost.Raise(h => h.ServiceSessionStarted += null));
+
+ sut.Perform();
}
}
}
diff --git a/SafeExamBrowser.Runtime.UnitTests/Operations/WRONG/SettingsDummy.txt b/SafeExamBrowser.Runtime.UnitTests/Operations/WRONG/SebClientSettings.seb
similarity index 100%
rename from SafeExamBrowser.Runtime.UnitTests/Operations/WRONG/SettingsDummy.txt
rename to SafeExamBrowser.Runtime.UnitTests/Operations/WRONG/SebClientSettings.seb
diff --git a/SafeExamBrowser.Runtime.UnitTests/SafeExamBrowser.Runtime.UnitTests.csproj b/SafeExamBrowser.Runtime.UnitTests/SafeExamBrowser.Runtime.UnitTests.csproj
index 13d95911..cdca3e7f 100644
--- a/SafeExamBrowser.Runtime.UnitTests/SafeExamBrowser.Runtime.UnitTests.csproj
+++ b/SafeExamBrowser.Runtime.UnitTests/SafeExamBrowser.Runtime.UnitTests.csproj
@@ -121,12 +121,12 @@
-
+
Always
-
-
+
+
Always
-
+
diff --git a/SafeExamBrowser.Runtime/Communication/RuntimeHost.cs b/SafeExamBrowser.Runtime/Communication/RuntimeHost.cs
index 37fab2ff..c115f70d 100644
--- a/SafeExamBrowser.Runtime/Communication/RuntimeHost.cs
+++ b/SafeExamBrowser.Runtime/Communication/RuntimeHost.cs
@@ -26,6 +26,10 @@ namespace SafeExamBrowser.Runtime.Communication
public event CommunicationEventHandler MessageBoxReplyReceived;
public event CommunicationEventHandler PasswordReceived;
public event CommunicationEventHandler 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)
diff --git a/SafeExamBrowser.Runtime/CompositionRoot.cs b/SafeExamBrowser.Runtime/CompositionRoot.cs
index 1ad224df..e39b2524 100644
--- a/SafeExamBrowser.Runtime/CompositionRoot.cs
+++ b/SafeExamBrowser.Runtime/CompositionRoot.cs
@@ -81,7 +81,7 @@ namespace SafeExamBrowser.Runtime
sessionOperations.Enqueue(new ConfigurationOperation(args, configuration, new HashAlgorithm(), logger, sessionContext));
sessionOperations.Enqueue(new ClientTerminationOperation(logger, processFactory, proxyFactory, runtimeHost, sessionContext, THIRTY_SECONDS));
sessionOperations.Enqueue(new KioskModeTerminationOperation(desktopFactory, explorerShell, logger, processFactory, sessionContext));
- sessionOperations.Enqueue(new ServiceOperation(logger, serviceProxy, sessionContext));
+ sessionOperations.Enqueue(new ServiceOperation(logger, runtimeHost, serviceProxy, sessionContext, THIRTY_SECONDS));
sessionOperations.Enqueue(new KioskModeOperation(desktopFactory, explorerShell, logger, processFactory, sessionContext));
sessionOperations.Enqueue(new ClientOperation(logger, processFactory, proxyFactory, runtimeHost, sessionContext, THIRTY_SECONDS));
sessionOperations.Enqueue(new SessionActivationOperation(logger, sessionContext));
@@ -170,7 +170,7 @@ namespace SafeExamBrowser.Runtime
private void InitializeLogging()
{
- var logFileWriter = new LogFileWriter(new DefaultLogFormatter(), appConfig.RuntimeLogFile);
+ var logFileWriter = new LogFileWriter(new DefaultLogFormatter(), appConfig.RuntimeLogFilePath);
logFileWriter.Initialize();
logger.LogLevel = LogLevel.Debug;
diff --git a/SafeExamBrowser.Runtime/Operations/ClientOperation.cs b/SafeExamBrowser.Runtime/Operations/ClientOperation.cs
index 7e1663e6..8ee838f7 100644
--- a/SafeExamBrowser.Runtime/Operations/ClientOperation.cs
+++ b/SafeExamBrowser.Runtime/Operations/ClientOperation.cs
@@ -96,7 +96,7 @@ namespace SafeExamBrowser.Runtime.Operations
private bool TryStartClient()
{
var clientExecutable = Context.Next.AppConfig.ClientExecutablePath;
- var clientLogFile = $"{'"' + Context.Next.AppConfig.ClientLogFile + '"'}";
+ var clientLogFile = $"{'"' + Context.Next.AppConfig.ClientLogFilePath + '"'}";
var clientLogLevel = Context.Next.Settings.LogLevel.ToString();
var runtimeHostUri = Context.Next.AppConfig.RuntimeAddress;
var startupToken = Context.Next.StartupToken.ToString("D");
diff --git a/SafeExamBrowser.Runtime/Operations/ConfigurationOperation.cs b/SafeExamBrowser.Runtime/Operations/ConfigurationOperation.cs
index 07a75cb9..4e998f27 100644
--- a/SafeExamBrowser.Runtime/Operations/ConfigurationOperation.cs
+++ b/SafeExamBrowser.Runtime/Operations/ConfigurationOperation.cs
@@ -27,15 +27,8 @@ namespace SafeExamBrowser.Runtime.Operations
private IHashAlgorithm hashAlgorithm;
private ILogger logger;
- private string AppDataFile
- {
- get { return Path.Combine(Context.Next.AppConfig.AppDataFolder, Context.Next.AppConfig.DefaultSettingsFileName); }
- }
-
- private string ProgramDataFile
- {
- get { return Path.Combine(Context.Next.AppConfig.ProgramDataFolder, Context.Next.AppConfig.DefaultSettingsFileName); }
- }
+ private string AppDataFilePath => Context.Next.AppConfig.AppDataFilePath;
+ private string ProgramDataFilePath => Context.Next.AppConfig.ProgramDataFilePath;
public override event ActionRequiredEventHandler ActionRequired;
public override event StatusChangedEventHandler StatusChanged;
@@ -119,16 +112,16 @@ namespace SafeExamBrowser.Runtime.Operations
if (source == UriSource.CommandLine)
{
- var hasAppDataFile = File.Exists(AppDataFile);
- var hasProgramDataFile = File.Exists(ProgramDataFile);
+ var hasAppDataFile = File.Exists(AppDataFilePath);
+ var hasProgramDataFile = File.Exists(ProgramDataFilePath);
if (hasProgramDataFile)
{
- status = TryLoadSettings(new Uri(ProgramDataFile, UriKind.Absolute), UriSource.ProgramData, out _, out settings);
+ status = TryLoadSettings(new Uri(ProgramDataFilePath, UriKind.Absolute), UriSource.ProgramData, out _, out settings);
}
else if (hasAppDataFile)
{
- status = TryLoadSettings(new Uri(AppDataFile, UriKind.Absolute), UriSource.AppData, out _, out settings);
+ status = TryLoadSettings(new Uri(AppDataFilePath, UriKind.Absolute), UriSource.AppData, out _, out settings);
}
if ((!hasProgramDataFile && !hasAppDataFile) || status == LoadStatus.Success)
@@ -412,18 +405,18 @@ namespace SafeExamBrowser.Runtime.Operations
logger.Info($"Found command-line argument for configuration resource: '{uri}', the URI is {(isValidUri ? "valid" : "invalid")}.");
}
- if (!isValidUri && File.Exists(ProgramDataFile))
+ if (!isValidUri && File.Exists(ProgramDataFilePath))
{
- isValidUri = Uri.TryCreate(ProgramDataFile, UriKind.Absolute, out uri);
+ isValidUri = Uri.TryCreate(ProgramDataFilePath, UriKind.Absolute, out uri);
source = UriSource.ProgramData;
- logger.Info($"Found configuration file in PROGRAMDATA directory: '{uri}'.");
+ logger.Info($"Found configuration file in program data directory: '{uri}'.");
}
- if (!isValidUri && File.Exists(AppDataFile))
+ if (!isValidUri && File.Exists(AppDataFilePath))
{
- isValidUri = Uri.TryCreate(AppDataFile, UriKind.Absolute, out uri);
+ isValidUri = Uri.TryCreate(AppDataFilePath, UriKind.Absolute, out uri);
source = UriSource.AppData;
- logger.Info($"Found configuration file in APPDATA directory: '{uri}'.");
+ logger.Info($"Found configuration file in app data directory: '{uri}'.");
}
return isValidUri;
diff --git a/SafeExamBrowser.Runtime/Operations/ServiceOperation.cs b/SafeExamBrowser.Runtime/Operations/ServiceOperation.cs
index 35936b6d..80dcb5f8 100644
--- a/SafeExamBrowser.Runtime/Operations/ServiceOperation.cs
+++ b/SafeExamBrowser.Runtime/Operations/ServiceOperation.cs
@@ -6,6 +6,9 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
+using System.Threading;
+using SafeExamBrowser.Contracts.Communication.Events;
+using SafeExamBrowser.Contracts.Communication.Hosts;
using SafeExamBrowser.Contracts.Communication.Proxies;
using SafeExamBrowser.Contracts.Configuration.Settings;
using SafeExamBrowser.Contracts.Core.OperationModel;
@@ -17,17 +20,25 @@ namespace SafeExamBrowser.Runtime.Operations
{
internal class ServiceOperation : SessionOperation
{
- private bool connected, mandatory;
private ILogger logger;
+ private IRuntimeHost runtimeHost;
private IServiceProxy service;
+ private int timeout_ms;
public override event ActionRequiredEventHandler ActionRequired { add { } remove { } }
public override event StatusChangedEventHandler StatusChanged;
- public ServiceOperation(ILogger logger, IServiceProxy service, SessionContext sessionContext) : base(sessionContext)
+ public ServiceOperation(
+ ILogger logger,
+ IRuntimeHost runtimeHost,
+ IServiceProxy service,
+ SessionContext sessionContext,
+ int timeout_ms) : base(sessionContext)
{
this.logger = logger;
+ this.runtimeHost = runtimeHost;
this.service = service;
+ this.timeout_ms = timeout_ms;
}
public override OperationResult Perform()
@@ -35,37 +46,38 @@ namespace SafeExamBrowser.Runtime.Operations
logger.Info($"Initializing service session...");
StatusChanged?.Invoke(TextKey.OperationStatus_InitializeServiceSession);
- mandatory = Context.Next.Settings.ServicePolicy == ServicePolicy.Mandatory;
- connected = service.Connect();
+ var success = TryEstablishConnection();
- if (mandatory && !connected)
+ if (service.IsConnected)
{
- logger.Error("Failed to initialize a service session since the service is mandatory but not available!");
-
- return OperationResult.Failed;
+ success = TryStartSession();
}
- service.Ignore = !connected;
- logger.Info($"The service is {(mandatory ? "mandatory" : "optional")} and {(connected ? "available." : "not available. All service-related operations will be ignored!")}");
-
- if (connected)
- {
- StartServiceSession();
- }
-
- return OperationResult.Success;
+ return success ? OperationResult.Success : OperationResult.Failed;
}
public override OperationResult Repeat()
{
- var result = Revert();
+ logger.Info($"Initializing new service session...");
+ StatusChanged?.Invoke(TextKey.OperationStatus_InitializeServiceSession);
- if (result != OperationResult.Success)
+ var success = false;
+
+ if (service.IsConnected)
{
- return result;
+ success = TryStopSession();
+ }
+ else
+ {
+ success = TryEstablishConnection();
}
- return Perform();
+ if (success && service.IsConnected)
+ {
+ success = TryStartSession();
+ }
+
+ return success ? OperationResult.Success : OperationResult.Failed;
}
public override OperationResult Revert()
@@ -73,33 +85,154 @@ namespace SafeExamBrowser.Runtime.Operations
logger.Info("Finalizing service session...");
StatusChanged?.Invoke(TextKey.OperationStatus_FinalizeServiceSession);
- if (connected)
- {
- StopServiceSession();
+ var success = true;
- var success = service.Disconnect();
+ if (service.IsConnected)
+ {
+ success &= TryStopSession();
+ success &= TryTerminateConnection();
+ }
+
+ return success ? OperationResult.Success : OperationResult.Failed;
+ }
+
+ private bool TryEstablishConnection()
+ {
+ var mandatory = Context.Next.Settings.ServicePolicy == ServicePolicy.Mandatory;
+ var connected = service.Connect();
+ var success = connected || !mandatory;
+
+ if (success)
+ {
+ service.Ignore = !connected;
+ logger.Info($"The service is {(mandatory ? "mandatory" : "optional")} and {(connected ? "connected." : "not connected. All service-related operations will be ignored!")}");
+ }
+ else
+ {
+ logger.Error("The service is mandatory but no connection could be established!");
+ }
+
+ return success;
+ }
+
+ private bool TryTerminateConnection()
+ {
+ var serviceEvent = new AutoResetEvent(false);
+ var serviceEventHandler = new CommunicationEventHandler(() => serviceEvent.Set());
+
+ runtimeHost.ServiceDisconnected += serviceEventHandler;
+
+ var success = service.Disconnect();
+
+ if (success)
+ {
+ logger.Info("Successfully disconnected from service. Waiting for service to disconnect...");
+
+ success = serviceEvent.WaitOne(timeout_ms);
if (success)
{
- logger.Info("Successfully disconnected from the service.");
+ logger.Info("Service disconnected successfully.");
}
else
{
- logger.Error("Failed to disconnect from the service!");
+ logger.Error($"Service failed to disconnect within {timeout_ms / 1000} seconds!");
}
}
+ else
+ {
+ logger.Error("Failed to disconnect from service!");
+ }
- return OperationResult.Success;
+ runtimeHost.ServiceDisconnected -= serviceEventHandler;
+
+ return success;
}
- private void StartServiceSession()
+ private bool TryStartSession()
{
- service.StartSession(Context.Next.Id, 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;
+
+ logger.Info("Starting new service session...");
+
+ var communication = service.StartSession(Context.Next.Id, Context.Next.Settings);
+
+ if (communication.Success)
+ {
+ serviceEvent.WaitOne(timeout_ms);
+
+ if (success)
+ {
+ 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!");
+ }
+ }
+ else
+ {
+ logger.Error("Failed to communicate session start to service!");
+ }
+
+ runtimeHost.ServiceFailed -= failureEventHandler;
+ runtimeHost.ServiceSessionStarted -= successEventHandler;
+
+ return success;
}
- private void StopServiceSession()
+ private bool TryStopSession()
{
- service.StopSession(Context.Current.Id);
+ 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;
+
+ logger.Info("Stopping current service session...");
+
+ var communication = service.StopSession(Context.Current.Id);
+
+ if (communication.Success)
+ {
+ serviceEvent.WaitOne(timeout_ms);
+
+ if (success)
+ {
+ 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!");
+ }
+ }
+ else
+ {
+ logger.Error("Failed to communicate session stop to service!");
+ }
+
+ runtimeHost.ServiceFailed -= failureEventHandler;
+ runtimeHost.ServiceSessionStopped -= successEventHandler;
+
+ return success;
}
}
}
diff --git a/SafeExamBrowser.Service/CompositionRoot.cs b/SafeExamBrowser.Service/CompositionRoot.cs
index e8e757c2..fa14dedf 100644
--- a/SafeExamBrowser.Service/CompositionRoot.cs
+++ b/SafeExamBrowser.Service/CompositionRoot.cs
@@ -6,13 +6,51 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
+using System;
+using System.IO;
+using SafeExamBrowser.Contracts.Logging;
+using SafeExamBrowser.Contracts.Service;
+using SafeExamBrowser.Logging;
+
namespace SafeExamBrowser.Service
{
internal class CompositionRoot
{
+ private ILogger logger;
+
+ internal IServiceController ServiceController { get; private set; }
+
internal void BuildObjectGraph()
{
+ logger = new Logger();
+ InitializeLogging();
+
+ ServiceController = new ServiceController();
+ }
+
+ internal void LogStartupInformation()
+ {
+ logger.Log($"# Service started at {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
+ logger.Log(string.Empty);
+ }
+
+ internal void LogShutdownInformation()
+ {
+ logger?.Log($"# Service terminated at {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
+ }
+
+ private void InitializeLogging()
+ {
+ var appDataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), nameof(SafeExamBrowser));
+ var logFolder = Path.Combine(appDataFolder, "Logs");
+ var logFilePrefix = DateTime.Now.ToString("yyyy-MM-dd\\_HH\\hmm\\mss\\s");
+ var logFilePath = Path.Combine(logFolder, $"{logFilePrefix}_Service.log");
+ var logFileWriter = new LogFileWriter(new DefaultLogFormatter(), logFilePath);
+
+ logFileWriter.Initialize();
+ logger.LogLevel = LogLevel.Debug;
+ logger.Subscribe(logFileWriter);
}
}
}
diff --git a/SafeExamBrowser.Service/SafeExamBrowser.Service.csproj b/SafeExamBrowser.Service/SafeExamBrowser.Service.csproj
index 4970e217..1a193fed 100644
--- a/SafeExamBrowser.Service/SafeExamBrowser.Service.csproj
+++ b/SafeExamBrowser.Service/SafeExamBrowser.Service.csproj
@@ -67,9 +67,20 @@
Component
+
+
+
+ {47da5933-bef8-4729-94e6-abde2db12262}
+ SafeExamBrowser.Contracts
+
+
+ {e107026c-2011-4552-a7d8-3a0d37881df6}
+ SafeExamBrowser.Logging
+
+
\ No newline at end of file
diff --git a/SafeExamBrowser.Service/Service.cs b/SafeExamBrowser.Service/Service.cs
index 2f69194b..5e218414 100644
--- a/SafeExamBrowser.Service/Service.cs
+++ b/SafeExamBrowser.Service/Service.cs
@@ -6,6 +6,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
+using System;
using System.ServiceProcess;
namespace SafeExamBrowser.Service
@@ -27,22 +28,22 @@ namespace SafeExamBrowser.Service
protected override void OnStart(string[] args)
{
- //instances = new CompositionRoot();
- //instances.BuildObjectGraph();
- //instances.LogStartupInformation();
+ instances = new CompositionRoot();
+ instances.BuildObjectGraph();
+ instances.LogStartupInformation();
- //var success = instances.ServiceController.TryStart();
+ var success = instances.ServiceController.TryStart();
- //if (!success)
- //{
- // Environment.Exit(-1);
- //}
+ if (!success)
+ {
+ Environment.Exit(-1);
+ }
}
protected override void OnStop()
{
- //instances?.ServiceController?.Terminate();
- //instances?.LogShutdownInformation();
+ instances?.ServiceController?.Terminate();
+ instances?.LogShutdownInformation();
}
}
}
diff --git a/SafeExamBrowser.Service/ServiceController.cs b/SafeExamBrowser.Service/ServiceController.cs
new file mode 100644
index 00000000..847dece5
--- /dev/null
+++ b/SafeExamBrowser.Service/ServiceController.cs
@@ -0,0 +1,25 @@
+/*
+ * 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.Service;
+
+namespace SafeExamBrowser.Service
+{
+ internal class ServiceController : IServiceController
+ {
+ public bool TryStart()
+ {
+ return true;
+ }
+
+ public void Terminate()
+ {
+
+ }
+ }
+}