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() + { + + } + } +}