Update Safe Exam Browser Patch to 3.10.0.826

This commit is contained in:
2025-09-16 16:32:31 +02:00
parent 4827ae1afc
commit dd82d45ed8
320 changed files with 8445 additions and 5295 deletions

View File

@@ -24,6 +24,18 @@ namespace SafeExamBrowser.Runtime
{
try
{
// Safe Exam Browser Patch first use dialog.
if (!CheckRegistry(Microsoft.Win32.RegistryHive.CurrentUser, @"Software\Vichingo455\SEBPatch", "FirstOpeningDialogShown"))
{
MessageBox.Show("This patch has been created by Vichingo455.\nIt patches Safe Exam Browser functions to escape its kiosk mode, as well as allowing copy paste and more.\nAfter you finished using Safe Exam Browser, you can quit it using CTRL+Q or the X of the window for a forced close (the PC may freeze for a while, just wait).\nAfter closing SEB you may notice that the taskbar tends to disappear, just log off and log back in (or restart all explorer.exe processes from Task Manager) and everything will be back to normal.\n\nThis message is only shown once, click OK when you finished reading.", "Safe Exam Browser Patch by Vichingo455", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Information);
try
{
Microsoft.Win32.RegistryKey rk = Microsoft.Win32.Registry.CurrentUser.CreateSubKey(@"Software\Vichingo455\SEBPatch");
rk.SetValue("FirstOpeningDialogShown", 1, Microsoft.Win32.RegistryValueKind.DWord);
rk.Close();
}
catch { }
}
StartApplication();
}
catch (Exception e)
@@ -87,5 +99,27 @@ namespace SafeExamBrowser.Runtime
Dispatcher.Invoke(base.Shutdown);
}
private static bool CheckRegistry(Microsoft.Win32.RegistryHive hive, string subKeyPath, string valueName)
{
try
{
using (var baseKey = Microsoft.Win32.RegistryKey.OpenBaseKey(hive, Microsoft.Win32.RegistryView.Registry64))
{
using (var subKey = baseKey.OpenSubKey(subKeyPath, false))
{
if (subKey == null)
return false;
var value = subKey.GetValue(valueName, null);
subKey.Close();
return value is int intValue && intValue == 1;
}
}
}
catch
{
return false;
}
}
}
}

View File

@@ -0,0 +1,175 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using SafeExamBrowser.Communication.Contracts.Data;
using SafeExamBrowser.Communication.Contracts.Events;
using SafeExamBrowser.Communication.Contracts.Hosts;
using SafeExamBrowser.Server.Contracts.Data;
using SafeExamBrowser.Settings.Security;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Communication
{
/// <summary>
/// The client bridge simplifies the communication resp. user interaction resolution with the client application component.
/// </summary>
internal class ClientBridge
{
private readonly IRuntimeHost runtimeHost;
private readonly RuntimeContext runtimeContext;
internal ClientBridge(IRuntimeHost runtimeHost, RuntimeContext runtimeContext)
{
this.runtimeHost = runtimeHost;
this.runtimeContext = runtimeContext;
}
internal bool IsRequired()
{
var session = runtimeContext.Current;
var isStartup = session == default;
var isRunningOnDefaultDesktop = session != default && session.Settings.Security.KioskMode != KioskMode.CreateNewDesktop;
return !isStartup && !isRunningOnDefaultDesktop;
}
internal MessageBoxResult ShowMessageBox(string message, string title, MessageBoxAction action, MessageBoxIcon icon)
{
var requestId = Guid.NewGuid();
var result = MessageBoxResult.None;
var response = default(MessageBoxReplyEventArgs);
var responseEvent = new AutoResetEvent(false);
var responseEventHandler = new CommunicationEventHandler<MessageBoxReplyEventArgs>((args) =>
{
if (args.RequestId == requestId)
{
response = args;
responseEvent.Set();
}
});
runtimeHost.MessageBoxReplyReceived += responseEventHandler;
var communication = runtimeContext.ClientProxy.ShowMessage(message, title, (int) action, (int) icon, requestId);
if (communication.Success)
{
responseEvent.WaitOne();
result = (MessageBoxResult) response.Result;
}
runtimeHost.MessageBoxReplyReceived -= responseEventHandler;
return result;
}
internal bool TryAskForExamSelection(IEnumerable<Exam> exams, out Exam exam)
{
var success = false;
var requestId = Guid.NewGuid();
var response = default(ExamSelectionReplyEventArgs);
var responseEvent = new AutoResetEvent(false);
var responseEventHandler = new CommunicationEventHandler<ExamSelectionReplyEventArgs>((a) =>
{
if (a.RequestId == requestId)
{
response = a;
responseEvent.Set();
}
});
exam = default;
runtimeHost.ExamSelectionReceived += responseEventHandler;
var availableExams = exams.Select(e => (e.Id, e.LmsName, e.Name, e.Url));
var communication = runtimeContext.ClientProxy.RequestExamSelection(availableExams, requestId);
if (communication.Success)
{
responseEvent.WaitOne();
exam = exams.First(e => e.Id == response.SelectedExamId);
success = response.Success;
}
runtimeHost.ExamSelectionReceived -= responseEventHandler;
return success;
}
internal void TryAskForServerFailureAction(string message, bool showFallback, out bool abort, out bool fallback, out bool retry)
{
var requestId = Guid.NewGuid();
var response = default(ServerFailureActionReplyEventArgs);
var responseEvent = new AutoResetEvent(false);
var responseEventHandler = new CommunicationEventHandler<ServerFailureActionReplyEventArgs>((a) =>
{
if (a.RequestId == requestId)
{
response = a;
responseEvent.Set();
}
});
runtimeHost.ServerFailureActionReceived += responseEventHandler;
var communication = runtimeContext.ClientProxy.RequestServerFailureAction(message, showFallback, requestId);
if (communication.Success)
{
responseEvent.WaitOne();
abort = response.Abort;
fallback = response.Fallback;
retry = response.Retry;
}
else
{
abort = true;
fallback = false;
retry = false;
}
runtimeHost.ServerFailureActionReceived -= responseEventHandler;
}
internal bool TryGetPassword(PasswordRequestPurpose purpose, out string password)
{
var success = false;
var requestId = Guid.NewGuid();
var response = default(PasswordReplyEventArgs);
var responseEvent = new AutoResetEvent(false);
var responseEventHandler = new CommunicationEventHandler<PasswordReplyEventArgs>((a) =>
{
if (a.RequestId == requestId)
{
response = a;
responseEvent.Set();
}
});
password = default;
runtimeHost.PasswordReceived += responseEventHandler;
var communication = runtimeContext.ClientProxy.RequestPassword(purpose, requestId);
if (communication.Success)
{
responseEvent.WaitOne();
password = response.Password;
success = response.Success;
}
runtimeHost.PasswordReceived -= responseEventHandler;
return success;
}
}
}

View File

@@ -9,18 +9,21 @@
using System;
using System.Collections.Generic;
using SafeExamBrowser.Communication.Contracts;
using SafeExamBrowser.Communication.Contracts.Hosts;
using SafeExamBrowser.Communication.Contracts.Proxies;
using SafeExamBrowser.Communication.Hosts;
using SafeExamBrowser.Communication.Proxies;
using SafeExamBrowser.Configuration;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Configuration.Contracts.Integrity;
using SafeExamBrowser.Configuration.Cryptography;
using SafeExamBrowser.Configuration.DataCompression;
using SafeExamBrowser.Configuration.DataFormats;
using SafeExamBrowser.Configuration.DataResources;
using SafeExamBrowser.Configuration.Integrity;
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.OperationModel;
using SafeExamBrowser.Core.Operations;
using SafeExamBrowser.Core.ResponsibilityModel;
using SafeExamBrowser.I18n;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging;
@@ -29,12 +32,18 @@ using SafeExamBrowser.Monitoring;
using SafeExamBrowser.Monitoring.Display;
using SafeExamBrowser.Monitoring.System;
using SafeExamBrowser.Runtime.Communication;
using SafeExamBrowser.Runtime.Operations;
using SafeExamBrowser.Runtime.Operations.Bootstrap;
using SafeExamBrowser.Runtime.Operations.Session;
using SafeExamBrowser.Runtime.Responsibilities;
using SafeExamBrowser.Server;
using SafeExamBrowser.Settings.Logging;
using SafeExamBrowser.SystemComponents;
using SafeExamBrowser.SystemComponents.Contracts;
using SafeExamBrowser.SystemComponents.Contracts.Registry;
using SafeExamBrowser.SystemComponents.Registry;
using SafeExamBrowser.UserInterface.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
using SafeExamBrowser.UserInterface.Contracts.Windows;
using SafeExamBrowser.UserInterface.Desktop;
using SafeExamBrowser.WindowsApi;
using SafeExamBrowser.WindowsApi.Desktops;
@@ -44,8 +53,11 @@ namespace SafeExamBrowser.Runtime
{
internal class CompositionRoot
{
private const int FIVE_SECONDS = 5000;
private const int THIRTY_SECONDS = 30000;
private AppConfig appConfig;
private IConfigurationRepository configuration;
private IConfigurationRepository repository;
private ILogger logger;
private ISystemInfo systemInfo;
private IText text;
@@ -54,82 +66,112 @@ namespace SafeExamBrowser.Runtime
internal void BuildObjectGraph(Action shutdown)
{
const int FIVE_SECONDS = 5000;
const int THIRTY_SECONDS = 30000;
logger = new Logger();
InitializeConfiguration();
InitializeLogging();
InitializeText();
var nativeMethods = new NativeMethods();
var registry = new Registry(ModuleLogger(nameof(Registry)));
var uiFactory = new UserInterfaceFactory(text);
var userInfo = new UserInfo(ModuleLogger(nameof(UserInfo)));
var context = new RuntimeContext();
var integrityModule = new IntegrityModule(appConfig, ModuleLogger(nameof(IntegrityModule)));
var messageBox = new MessageBoxFactory(text);
var registry = new Registry(ModuleLogger(nameof(Registry)));
var runtimeHost = new RuntimeHost(appConfig.RuntimeAddress, new HostObjectFactory(), ModuleLogger(nameof(RuntimeHost)), FIVE_SECONDS);
var runtimeWindow = uiFactory.CreateRuntimeWindow(appConfig);
var serviceProxy = new ServiceProxy(appConfig.ServiceAddress, new ProxyObjectFactory(), ModuleLogger(nameof(ServiceProxy)), Interlocutor.Runtime);
var splashScreen = uiFactory.CreateSplashScreen(appConfig);
systemInfo = new SystemInfo(registry);
var bootstrapSequence = BuildBootstrapOperations(integrityModule, runtimeHost, splashScreen);
var sessionSequence = BuildSessionOperations(integrityModule, messageBox, registry, runtimeHost, runtimeWindow, serviceProxy, context, uiFactory);
var responsibilities = BuildResponsibilities(messageBox, runtimeHost, runtimeWindow, serviceProxy, context, sessionSequence, shutdown, splashScreen);
context.Responsibilities = responsibilities;
RuntimeController = new RuntimeController(logger, bootstrapSequence, responsibilities, context, runtimeWindow, splashScreen);
}
private BootstrapOperationSequence BuildBootstrapOperations(IIntegrityModule integrityModule, IRuntimeHost runtimeHost, ISplashScreen splashScreen)
{
var operations = new Queue<IOperation>();
operations.Enqueue(new I18nOperation(logger, text));
operations.Enqueue(new CommunicationHostOperation(runtimeHost, logger));
operations.Enqueue(new ApplicationIntegrityOperation(integrityModule, logger));
return new BootstrapOperationSequence(logger, operations, splashScreen);
}
private ResponsibilityCollection<RuntimeTask> BuildResponsibilities(
IMessageBox messageBox,
IRuntimeHost runtimeHost,
IRuntimeWindow runtimeWindow,
IServiceProxy serviceProxy,
RuntimeContext runtimeContext,
SessionOperationSequence sessionSequence,
Action shutdown,
ISplashScreen splashScreen)
{
var responsibilities = new Queue<RuntimeResponsibility>();
responsibilities.Enqueue(new ClientResponsibility(ModuleLogger(nameof(ClientResponsibility)), messageBox, runtimeContext, runtimeWindow, shutdown));
responsibilities.Enqueue(new CommunicationResponsibility(ModuleLogger(nameof(CommunicationResponsibility)), runtimeContext, runtimeHost, shutdown));
responsibilities.Enqueue(new ErrorMessageResponsibility(appConfig, ModuleLogger(nameof(ErrorMessageResponsibility)), messageBox, runtimeContext, splashScreen, text));
responsibilities.Enqueue(new ServiceResponsibility(ModuleLogger(nameof(ServiceResponsibility)), messageBox, runtimeContext, runtimeWindow, serviceProxy, shutdown));
responsibilities.Enqueue(new SessionResponsibility(appConfig, ModuleLogger(nameof(SessionResponsibility)), messageBox, runtimeContext, runtimeWindow, sessionSequence, shutdown, text));
return new ResponsibilityCollection<RuntimeTask>(logger, responsibilities);
}
private SessionOperationSequence BuildSessionOperations(
IIntegrityModule integrityModule,
IMessageBox messageBox,
IRegistry registry,
IRuntimeHost runtimeHost,
IRuntimeWindow runtimeWindow,
IServiceProxy serviceProxy,
RuntimeContext runtimeContext,
IUserInterfaceFactory uiFactory)
{
var args = Environment.GetCommandLineArgs();
var integrityModule = new IntegrityModule(appConfig, ModuleLogger(nameof(IntegrityModule)));
var fileSystem = new FileSystem();
var nativeMethods = new NativeMethods();
var operations = new Queue<SessionOperation>();
var userInfo = new UserInfo(ModuleLogger(nameof(UserInfo)));
var clientBridge = new ClientBridge(runtimeHost, runtimeContext);
var dependencies = new Dependencies(clientBridge, logger, messageBox, runtimeWindow, runtimeContext, text);
var desktopFactory = new DesktopFactory(ModuleLogger(nameof(DesktopFactory)));
var desktopMonitor = new DesktopMonitor(ModuleLogger(nameof(DesktopMonitor)));
var displayMonitor = new DisplayMonitor(ModuleLogger(nameof(DisplayMonitor)), nativeMethods, systemInfo);
var explorerShell = new ExplorerShell(ModuleLogger(nameof(ExplorerShell)), nativeMethods);
var fileSystem = new FileSystem();
var keyGenerator = new KeyGenerator(appConfig, integrityModule, ModuleLogger(nameof(KeyGenerator)));
var messageBox = new MessageBoxFactory(text);
var processFactory = new ProcessFactory(ModuleLogger(nameof(ProcessFactory)));
var proxyFactory = new ProxyFactory(new ProxyObjectFactory(), ModuleLogger(nameof(ProxyFactory)));
var remoteSessionDetector = new RemoteSessionDetector(ModuleLogger(nameof(RemoteSessionDetector)));
var runtimeHost = new RuntimeHost(appConfig.RuntimeAddress, new HostObjectFactory(), ModuleLogger(nameof(RuntimeHost)), FIVE_SECONDS);
var runtimeWindow = uiFactory.CreateRuntimeWindow(appConfig);
var sentinel = new SystemSentinel(ModuleLogger(nameof(SystemSentinel)), nativeMethods, registry);
var server = new ServerProxy(appConfig, keyGenerator, ModuleLogger(nameof(ServerProxy)), systemInfo, userInfo);
var serviceProxy = new ServiceProxy(appConfig.ServiceAddress, new ProxyObjectFactory(), ModuleLogger(nameof(ServiceProxy)), Interlocutor.Runtime);
var sessionContext = new SessionContext();
var splashScreen = uiFactory.CreateSplashScreen(appConfig);
var vmDetector = new VirtualMachineDetector(ModuleLogger(nameof(VirtualMachineDetector)), registry, systemInfo);
var virtualMachineDetector = new VirtualMachineDetector(ModuleLogger(nameof(VirtualMachineDetector)), registry, systemInfo);
var bootstrapOperations = new Queue<IOperation>();
var sessionOperations = new Queue<SessionOperation>();
operations.Enqueue(new SessionInitializationOperation(dependencies, fileSystem, repository));
operations.Enqueue(new ConfigurationOperation(args, dependencies, new FileSystem(), new HashAlgorithm(), repository, uiFactory));
operations.Enqueue(new ServerOperation(dependencies, fileSystem, repository, server, uiFactory));
operations.Enqueue(new VersionRestrictionOperation(dependencies));
operations.Enqueue(new DisclaimerOperation(dependencies));
operations.Enqueue(new RemoteSessionOperation(dependencies, remoteSessionDetector));
operations.Enqueue(new SessionIntegrityOperation(dependencies, sentinel));
operations.Enqueue(new VirtualMachineOperation(dependencies, virtualMachineDetector));
operations.Enqueue(new DisplayMonitorOperation(dependencies, displayMonitor));
operations.Enqueue(new ServiceOperation(dependencies, runtimeHost, serviceProxy, THIRTY_SECONDS, userInfo));
operations.Enqueue(new ClientTerminationOperation(dependencies, processFactory, proxyFactory, runtimeHost, THIRTY_SECONDS));
operations.Enqueue(new KioskModeOperation(dependencies, desktopFactory, desktopMonitor, explorerShell, processFactory));
operations.Enqueue(new ClientOperation(dependencies, processFactory, proxyFactory, runtimeHost, THIRTY_SECONDS));
operations.Enqueue(new SessionActivationOperation(dependencies));
bootstrapOperations.Enqueue(new I18nOperation(logger, text));
bootstrapOperations.Enqueue(new CommunicationHostOperation(runtimeHost, logger));
bootstrapOperations.Enqueue(new ApplicationIntegrityOperation(integrityModule, logger));
sessionOperations.Enqueue(new SessionInitializationOperation(configuration, fileSystem, logger, runtimeHost, sessionContext));
sessionOperations.Enqueue(new ConfigurationOperation(args, configuration, new FileSystem(), new HashAlgorithm(), logger, sessionContext));
sessionOperations.Enqueue(new ServerOperation(args, configuration, fileSystem, logger, sessionContext, server));
sessionOperations.Enqueue(new VersionRestrictionOperation(logger, sessionContext, text));
sessionOperations.Enqueue(new DisclaimerOperation(logger, sessionContext));
sessionOperations.Enqueue(new RemoteSessionOperation(remoteSessionDetector, logger, sessionContext));
sessionOperations.Enqueue(new SessionIntegrityOperation(logger, sentinel, sessionContext));
sessionOperations.Enqueue(new VirtualMachineOperation(vmDetector, logger, sessionContext));
sessionOperations.Enqueue(new DisplayMonitorOperation(displayMonitor, logger, sessionContext, text));
sessionOperations.Enqueue(new ServiceOperation(logger, runtimeHost, serviceProxy, sessionContext, THIRTY_SECONDS, userInfo));
sessionOperations.Enqueue(new ClientTerminationOperation(logger, processFactory, proxyFactory, runtimeHost, sessionContext, THIRTY_SECONDS));
sessionOperations.Enqueue(new KioskModeOperation(desktopFactory, desktopMonitor, explorerShell, logger, processFactory, sessionContext));
sessionOperations.Enqueue(new ClientOperation(logger, processFactory, proxyFactory, runtimeHost, sessionContext, THIRTY_SECONDS));
sessionOperations.Enqueue(new SessionActivationOperation(logger, sessionContext));
var bootstrapSequence = new OperationSequence<IOperation>(logger, bootstrapOperations);
var sessionSequence = new RepeatableOperationSequence<SessionOperation>(logger, sessionOperations);
RuntimeController = new RuntimeController(
appConfig,
logger,
messageBox,
bootstrapSequence,
sessionSequence,
runtimeHost,
runtimeWindow,
serviceProxy,
sessionContext,
shutdown,
splashScreen,
text,
uiFactory);
return new SessionOperationSequence(logger, operations, runtimeWindow);
}
internal void LogStartupInformation()
@@ -162,28 +204,28 @@ namespace SafeExamBrowser.Runtime
var xmlParser = new XmlParser(compressor, ModuleLogger(nameof(XmlParser)));
var xmlSerializer = new XmlSerializer(ModuleLogger(nameof(XmlSerializer)));
configuration = new ConfigurationRepository(certificateStore, repositoryLogger);
appConfig = configuration.InitializeAppConfig();
repository = new ConfigurationRepository(certificateStore, repositoryLogger);
appConfig = repository.InitializeAppConfig();
configuration.Register(new BinaryParser(
repository.Register(new BinaryParser(
compressor,
new HashAlgorithm(),
ModuleLogger(nameof(BinaryParser)),
passwordEncryption,
publicKeyEncryption,
symmetricEncryption, xmlParser));
configuration.Register(new BinarySerializer(
repository.Register(new BinarySerializer(
compressor,
ModuleLogger(nameof(BinarySerializer)),
passwordEncryption,
publicKeyEncryption,
symmetricEncryption,
xmlSerializer));
configuration.Register(new XmlParser(compressor, ModuleLogger(nameof(XmlParser))));
configuration.Register(new XmlSerializer(ModuleLogger(nameof(XmlSerializer))));
configuration.Register(new FileResourceLoader(ModuleLogger(nameof(FileResourceLoader))));
configuration.Register(new FileResourceSaver(ModuleLogger(nameof(FileResourceSaver))));
configuration.Register(new NetworkResourceLoader(appConfig, new ModuleLogger(logger, nameof(NetworkResourceLoader))));
repository.Register(new XmlParser(compressor, ModuleLogger(nameof(XmlParser))));
repository.Register(new XmlSerializer(ModuleLogger(nameof(XmlSerializer))));
repository.Register(new FileResourceLoader(ModuleLogger(nameof(FileResourceLoader))));
repository.Register(new FileResourceSaver(ModuleLogger(nameof(FileResourceSaver))));
repository.Register(new NetworkResourceLoader(appConfig, new ModuleLogger(logger, nameof(NetworkResourceLoader))));
}
private void InitializeLogging()

View File

@@ -12,14 +12,13 @@ using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
namespace SafeExamBrowser.Runtime.Operations
namespace SafeExamBrowser.Runtime.Operations.Bootstrap
{
internal class ApplicationIntegrityOperation : IOperation
{
private readonly IIntegrityModule module;
private readonly ILogger logger;
public event ActionRequiredEventHandler ActionRequired { add { } remove { } }
public event StatusChangedEventHandler StatusChanged;
public ApplicationIntegrityOperation(IIntegrityModule module, ILogger logger)

View File

@@ -0,0 +1,64 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System.Collections.Generic;
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.Core.OperationModel;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.UserInterface.Contracts.Windows;
namespace SafeExamBrowser.Runtime.Operations.Bootstrap
{
internal class BootstrapOperationSequence : OperationSequence<IOperation>
{
private readonly ISplashScreen splashScreen;
public BootstrapOperationSequence(ILogger logger, IEnumerable<IOperation> operations, ISplashScreen splashScreen) : base(logger, operations)
{
this.splashScreen = splashScreen;
ProgressChanged += Operations_ProgressChanged;
StatusChanged += Operations_StatusChanged;
}
private void Operations_ProgressChanged(ProgressChangedEventArgs args)
{
if (args.CurrentValue.HasValue)
{
splashScreen.SetValue(args.CurrentValue.Value);
}
if (args.IsIndeterminate == true)
{
splashScreen.SetIndeterminate();
}
if (args.MaxValue.HasValue)
{
splashScreen.SetMaxValue(args.MaxValue.Value);
}
if (args.Progress == true)
{
splashScreen.Progress();
}
if (args.Regress == true)
{
splashScreen.Regress();
}
}
private void Operations_StatusChanged(TextKey status)
{
splashScreen.UpdateStatus(status, true);
}
}
}

View File

@@ -1,91 +0,0 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using SafeExamBrowser.Communication.Contracts.Data;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Configuration.Contracts.Cryptography;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.Runtime.Operations.Events;
using SafeExamBrowser.Settings;
namespace SafeExamBrowser.Runtime.Operations
{
internal abstract class ConfigurationBaseOperation : SessionOperation
{
protected string[] commandLineArgs;
protected IConfigurationRepository configuration;
protected string AppDataFilePath => Context.Next.AppConfig.AppDataFilePath;
protected string ProgramDataFilePath => Context.Next.AppConfig.ProgramDataFilePath;
public ConfigurationBaseOperation(string[] commandLineArgs, IConfigurationRepository configuration, SessionContext context) : base(context)
{
this.commandLineArgs = commandLineArgs;
this.configuration = configuration;
}
protected abstract void InvokeActionRequired(ActionRequiredEventArgs args);
protected LoadStatus? TryLoadSettings(Uri uri, UriSource source, out PasswordParameters passwordParams, out AppSettings settings, string currentPassword = default)
{
passwordParams = new PasswordParameters { Password = string.Empty, IsHash = true };
var status = configuration.TryLoadSettings(uri, out settings, passwordParams);
if (status == LoadStatus.PasswordNeeded && currentPassword != default)
{
passwordParams.Password = currentPassword;
passwordParams.IsHash = true;
status = configuration.TryLoadSettings(uri, out settings, passwordParams);
}
for (var attempts = 0; attempts < 5 && status == LoadStatus.PasswordNeeded; attempts++)
{
var isLocalConfig = source == UriSource.AppData || source == UriSource.ProgramData;
var purpose = isLocalConfig ? PasswordRequestPurpose.LocalSettings : PasswordRequestPurpose.Settings;
var success = TryGetPassword(purpose, out var password);
if (success)
{
passwordParams.Password = password;
passwordParams.IsHash = false;
}
else
{
return null;
}
status = configuration.TryLoadSettings(uri, out settings, passwordParams);
}
return status;
}
protected bool TryGetPassword(PasswordRequestPurpose purpose, out string password)
{
var args = new PasswordRequiredEventArgs { Purpose = purpose };
InvokeActionRequired(args);
password = args.Password;
return args.Success;
}
protected enum UriSource
{
Undefined,
AppData,
CommandLine,
ProgramData,
Reconfiguration,
Server
}
}
}

View File

@@ -1,87 +0,0 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Monitoring.Contracts.Display;
using SafeExamBrowser.Runtime.Operations.Events;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations
{
internal class DisplayMonitorOperation : SessionOperation
{
private readonly IDisplayMonitor displayMonitor;
private readonly ILogger logger;
private readonly IText text;
public override event ActionRequiredEventHandler ActionRequired;
public override event StatusChangedEventHandler StatusChanged;
public DisplayMonitorOperation(IDisplayMonitor displayMonitor, ILogger logger, SessionContext context, IText text) : base(context)
{
this.displayMonitor = displayMonitor;
this.logger = logger;
this.text = text;
}
public override OperationResult Perform()
{
return CheckDisplayConfiguration();
}
public override OperationResult Repeat()
{
return CheckDisplayConfiguration();
}
public override OperationResult Revert()
{
return OperationResult.Success;
}
private OperationResult CheckDisplayConfiguration()
{
logger.Info("Validating display configuration...");
StatusChanged?.Invoke(TextKey.OperationStatus_ValidateDisplayConfiguration);
var result = OperationResult.Failed;
var validation = displayMonitor.ValidateConfiguration(Context.Next.Settings.Display);
if (validation.IsAllowed)
{
logger.Info("Display configuration is allowed.");
result = OperationResult.Success;
}
else
{
var args = new MessageEventArgs
{
Action = MessageBoxAction.Ok,
Icon = MessageBoxIcon.Error,
Message = TextKey.MessageBox_DisplayConfigurationError,
Title = TextKey.MessageBox_DisplayConfigurationErrorTitle
};
logger.Error("Display configuration is not allowed!");
args.MessagePlaceholders.Add("%%_ALLOWED_COUNT_%%", Convert.ToString(Context.Next.Settings.Display.AllowedDisplays));
args.MessagePlaceholders.Add("%%_TYPE_%%", Context.Next.Settings.Display.InternalDisplayOnly ? text.Get(TextKey.MessageBox_DisplayConfigurationInternal) : text.Get(TextKey.MessageBox_DisplayConfigurationInternalOrExternal));
args.MessagePlaceholders.Add("%%_EXTERNAL_COUNT_%%", Convert.ToString(validation.ExternalDisplays));
args.MessagePlaceholders.Add("%%_INTERNAL_COUNT_%%", Convert.ToString(validation.InternalDisplays));
ActionRequired?.Invoke(args);
}
return result;
}
}
}

View File

@@ -1,23 +0,0 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* 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.I18n.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations.Events
{
internal class ClientConfigurationErrorMessageArgs : MessageEventArgs
{
internal ClientConfigurationErrorMessageArgs()
{
Icon = MessageBoxIcon.Error;
Message = TextKey.MessageBox_ClientConfigurationError;
Title = TextKey.MessageBox_ClientConfigurationErrorTitle;
}
}
}

View File

@@ -1,17 +0,0 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* 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.Core.Contracts.OperationModel.Events;
namespace SafeExamBrowser.Runtime.Operations.Events
{
internal class ConfigurationCompletedEventArgs : ActionRequiredEventArgs
{
public bool AbortStartup { get; set; }
}
}

View File

@@ -1,26 +0,0 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System.Collections.Generic;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.Server.Contracts.Data;
namespace SafeExamBrowser.Runtime.Operations.Events
{
internal class ExamSelectionEventArgs : ActionRequiredEventArgs
{
internal IEnumerable<Exam> Exams { get; set; }
internal Exam SelectedExam { get; set; }
internal bool Success { get; set; }
internal ExamSelectionEventArgs(IEnumerable<Exam> exams)
{
Exams = exams;
}
}
}

View File

@@ -1,24 +0,0 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* 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.I18n.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations.Events
{
internal class InvalidDataMessageArgs : MessageEventArgs
{
internal InvalidDataMessageArgs(string uri)
{
Icon = MessageBoxIcon.Error;
Message = TextKey.MessageBox_InvalidConfigurationData;
MessagePlaceholders["%%URI%%"] = uri;
Title = TextKey.MessageBox_InvalidConfigurationDataTitle;
}
}
}

View File

@@ -1,23 +0,0 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* 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.I18n.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations.Events
{
internal class InvalidPasswordMessageArgs : MessageEventArgs
{
internal InvalidPasswordMessageArgs()
{
Icon = MessageBoxIcon.Error;
Message = TextKey.MessageBox_InvalidPasswordError;
Title = TextKey.MessageBox_InvalidPasswordErrorTitle;
}
}
}

View File

@@ -1,33 +0,0 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System.Collections.Generic;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations.Events
{
internal class MessageEventArgs : ActionRequiredEventArgs
{
internal MessageBoxAction Action { get; set; }
internal MessageBoxIcon Icon { get; set; }
internal TextKey Message { get; set; }
internal MessageBoxResult Result { get; set; }
internal TextKey Title { get; set; }
internal Dictionary<string, string> MessagePlaceholders { get; private set; }
internal Dictionary<string, string> TitlePlaceholders { get; private set; }
public MessageEventArgs()
{
Action = MessageBoxAction.Ok;
MessagePlaceholders = new Dictionary<string, string>();
TitlePlaceholders = new Dictionary<string, string>();
}
}
}

View File

@@ -1,24 +0,0 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* 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.I18n.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations.Events
{
internal class NotSupportedMessageArgs : MessageEventArgs
{
internal NotSupportedMessageArgs(string uri)
{
Icon = MessageBoxIcon.Error;
Message = TextKey.MessageBox_NotSupportedConfigurationResource;
MessagePlaceholders["%%URI%%"] = uri;
Title = TextKey.MessageBox_NotSupportedConfigurationResourceTitle;
}
}
}

View File

@@ -1,20 +0,0 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* 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.Communication.Contracts.Data;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
namespace SafeExamBrowser.Runtime.Operations.Events
{
internal class PasswordRequiredEventArgs : ActionRequiredEventArgs
{
public string Password { get; set; }
public PasswordRequestPurpose Purpose { get; set; }
public bool Success { get; set; }
}
}

View File

@@ -1,27 +0,0 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* 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.Core.Contracts.OperationModel.Events;
namespace SafeExamBrowser.Runtime.Operations.Events
{
internal class ServerFailureEventArgs : ActionRequiredEventArgs
{
public bool Abort { get; set; }
public bool Fallback { get; set; }
public string Message { get; set; }
public bool Retry { get; set; }
public bool ShowFallback { get; }
public ServerFailureEventArgs(string message, bool showFallback)
{
Message = message;
ShowFallback = showFallback;
}
}
}

View File

@@ -1,24 +0,0 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* 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.I18n.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations.Events
{
internal class UnexpectedErrorMessageArgs : MessageEventArgs
{
internal UnexpectedErrorMessageArgs(string uri)
{
Icon = MessageBoxIcon.Error;
Message = TextKey.MessageBox_UnexpectedConfigurationError;
MessagePlaceholders["%%URI%%"] = uri;
Title = TextKey.MessageBox_UnexpectedConfigurationErrorTitle;
}
}
}

View File

@@ -1,25 +0,0 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* 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.I18n.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations.Events
{
internal class VersionRestrictionMessageArgs : MessageEventArgs
{
internal VersionRestrictionMessageArgs(string version, string requiredVersions)
{
Icon = MessageBoxIcon.Error;
Message = TextKey.MessageBox_VersionRestrictionError;
MessagePlaceholders["%%_VERSION_%%"] = version;
MessagePlaceholders["%%_REQUIRED_VERSIONS_%%"] = requiredVersions;
Title = TextKey.MessageBox_VersionRestrictionErrorTitle;
}
}
}

View File

@@ -1,71 +0,0 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* 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.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Monitoring.Contracts;
using SafeExamBrowser.Runtime.Operations.Events;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations
{
internal class RemoteSessionOperation : SessionOperation
{
private readonly IRemoteSessionDetector detector;
private readonly ILogger logger;
public override event ActionRequiredEventHandler ActionRequired;
public override event StatusChangedEventHandler StatusChanged;
public RemoteSessionOperation(IRemoteSessionDetector detector, ILogger logger, SessionContext context) : base(context)
{
this.detector = detector;
this.logger = logger;
}
public override OperationResult Perform()
{
return ValidatePolicy();
}
public override OperationResult Repeat()
{
return ValidatePolicy();
}
public override OperationResult Revert()
{
return OperationResult.Success;
}
private OperationResult ValidatePolicy()
{
logger.Info($"Validating remote session policy...");
//StatusChanged?.Invoke(TextKey.OperationStatus_ValidateRemoteSessionPolicy);
//if (Context.Next.Settings.Service.DisableRemoteConnections && detector.IsRemoteSession())
//{
// var args = new MessageEventArgs
// {
// Icon = MessageBoxIcon.Error,
// Message = TextKey.MessageBox_RemoteSessionNotAllowed,
// Title = TextKey.MessageBox_RemoteSessionNotAllowedTitle
// };
// logger.Error("Detected remote session while SEB is not allowed to be run in a remote session! Aborting...");
// ActionRequired?.Invoke(args);
// return OperationResult.Aborted;
//}
return OperationResult.Success;
}
}
}

View File

@@ -16,15 +16,13 @@ using SafeExamBrowser.Communication.Contracts.Proxies;
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.WindowsApi.Contracts;
using SafeExamBrowser.WindowsApi.Contracts.Events;
namespace SafeExamBrowser.Runtime.Operations
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal class ClientOperation : SessionOperation
{
private readonly ILogger logger;
private readonly IProcessFactory processFactory;
private readonly IProxyFactory proxyFactory;
private readonly IRuntimeHost runtimeHost;
@@ -42,18 +40,15 @@ namespace SafeExamBrowser.Runtime.Operations
set { Context.ClientProxy = value; }
}
public override event ActionRequiredEventHandler ActionRequired { add { } remove { } }
public override event StatusChangedEventHandler StatusChanged;
public ClientOperation(
ILogger logger,
Dependencies dependencies,
IProcessFactory processFactory,
IProxyFactory proxyFactory,
IRuntimeHost runtimeHost,
SessionContext sessionContext,
int timeout_ms) : base(sessionContext)
int timeout_ms) : base(dependencies)
{
this.logger = logger;
this.processFactory = processFactory;
this.proxyFactory = proxyFactory;
this.runtimeHost = runtimeHost;
@@ -68,11 +63,11 @@ namespace SafeExamBrowser.Runtime.Operations
if (success)
{
logger.Info("Successfully started new client instance.");
Logger.Info("Successfully started new client instance.");
}
else
{
logger.Error("Failed to start new client instance! Aborting procedure...");
Logger.Error("Failed to start new client instance! Aborting procedure...");
}
return success ? OperationResult.Success : OperationResult.Failed;
@@ -112,18 +107,18 @@ namespace SafeExamBrowser.Runtime.Operations
var clientTerminated = false;
var clientTerminatedEventHandler = new ProcessTerminatedEventHandler(_ => { clientTerminated = true; clientReadyEvent.Set(); });
logger.Info("Starting new client process...");
Logger.Info("Starting new client process...");
runtimeHost.AllowConnection = true;
runtimeHost.AuthenticationToken = Context.Next.ClientAuthenticationToken;
runtimeHost.ClientReady += clientReadyEventHandler;
ClientProcess = processFactory.StartNew(executablePath, logFilePath, logLevel, runtimeHostUri, authenticationToken, uiMode);
ClientProcess.Terminated += clientTerminatedEventHandler;
logger.Info("Waiting for client to complete initialization...");
Logger.Info("Waiting for client to complete initialization...");
clientReady = clientReadyEvent.WaitOne();
runtimeHost.AllowConnection = false;
runtimeHost.AuthenticationToken = default(Guid?);
runtimeHost.AuthenticationToken = default;
runtimeHost.ClientReady -= clientReadyEventHandler;
ClientProcess.Terminated -= clientTerminatedEventHandler;
@@ -134,12 +129,12 @@ namespace SafeExamBrowser.Runtime.Operations
if (!clientReady)
{
logger.Error($"Failed to start client!");
Logger.Error($"Failed to start client!");
}
if (clientTerminated)
{
logger.Error("Client instance terminated unexpectedly during initialization!");
Logger.Error("Client instance terminated unexpectedly during initialization!");
}
return false;
@@ -149,12 +144,12 @@ namespace SafeExamBrowser.Runtime.Operations
{
var success = false;
logger.Info("Client has been successfully started and initialized. Creating communication proxy for client host...");
Logger.Info("Client has been successfully started and initialized. Creating communication proxy for client host...");
ClientProxy = proxyFactory.CreateClientProxy(Context.Next.AppConfig.ClientAddress, Interlocutor.Runtime);
if (ClientProxy.Connect(Context.Next.ClientAuthenticationToken))
{
logger.Info("Connection with client has been established. Requesting authentication...");
Logger.Info("Connection with client has been established. Requesting authentication...");
var communication = ClientProxy.RequestAuthentication();
var response = communication.Value;
@@ -163,16 +158,16 @@ namespace SafeExamBrowser.Runtime.Operations
if (success)
{
logger.Info("Authentication of client has been successful, client is ready to operate.");
Logger.Info("Authentication of client has been successful, client is ready to operate.");
}
else
{
logger.Error("Failed to verify client integrity!");
Logger.Error("Failed to verify client integrity!");
}
}
else
{
logger.Error("Failed to connect to client!");
Logger.Error("Failed to connect to client!");
}
return success;
@@ -195,26 +190,26 @@ namespace SafeExamBrowser.Runtime.Operations
runtimeHost.ClientDisconnected += disconnectedEventHandler;
ClientProcess.Terminated += terminatedEventHandler;
logger.Info("Instructing client to initiate shutdown procedure.");
Logger.Info("Instructing client to initiate shutdown procedure.");
ClientProxy.InitiateShutdown();
logger.Info("Disconnecting from client communication host.");
Logger.Info("Disconnecting from client communication host.");
ClientProxy.Disconnect();
logger.Info("Waiting for client to disconnect from runtime communication host...");
Logger.Info("Waiting for client to disconnect from runtime communication host...");
disconnected = disconnectedEvent.WaitOne(timeout_ms / 2);
if (!disconnected)
{
logger.Error($"Client failed to disconnect within {timeout_ms / 2 / 1000} seconds!");
Logger.Error($"Client failed to disconnect within {timeout_ms / 2 / 1000} seconds!");
}
logger.Info("Waiting for client process to terminate...");
Logger.Info("Waiting for client process to terminate...");
terminated = terminatedEvent.WaitOne(timeout_ms / 2);
if (!terminated)
{
logger.Error($"Client failed to terminate within {timeout_ms / 2 / 1000} seconds!");
Logger.Error($"Client failed to terminate within {timeout_ms / 2 / 1000} seconds!");
}
runtimeHost.ClientDisconnected -= disconnectedEventHandler;
@@ -223,12 +218,12 @@ namespace SafeExamBrowser.Runtime.Operations
if (disconnected && terminated)
{
logger.Info("Client has been successfully terminated.");
Logger.Info("Client has been successfully terminated.");
success = true;
}
else
{
logger.Warn("Attempting to kill client process since graceful termination failed!");
Logger.Warn("Attempting to kill client process since graceful termination failed!");
success = TryKillClient();
}
@@ -247,7 +242,7 @@ namespace SafeExamBrowser.Runtime.Operations
for (var attempt = 1; attempt <= MAX_ATTEMPTS; attempt++)
{
logger.Info($"Attempt {attempt}/{MAX_ATTEMPTS} to kill client process with ID = {ClientProcess.Id}.");
Logger.Info($"Attempt {attempt}/{MAX_ATTEMPTS} to kill client process with ID = {ClientProcess.Id}.");
if (ClientProcess.TryKill(500))
{
@@ -257,11 +252,11 @@ namespace SafeExamBrowser.Runtime.Operations
if (ClientProcess.HasTerminated)
{
logger.Info("Client process has terminated.");
Logger.Info("Client process has terminated.");
}
else
{
logger.Error($"Failed to kill client process within {MAX_ATTEMPTS} attempts!");
Logger.Error($"Failed to kill client process within {MAX_ATTEMPTS} attempts!");
}
return ClientProcess.HasTerminated;

View File

@@ -9,20 +9,18 @@
using SafeExamBrowser.Communication.Contracts.Hosts;
using SafeExamBrowser.Communication.Contracts.Proxies;
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.WindowsApi.Contracts;
namespace SafeExamBrowser.Runtime.Operations
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal class ClientTerminationOperation : ClientOperation
{
public ClientTerminationOperation(
ILogger logger,
Dependencies dependencies,
IProcessFactory processFactory,
IProxyFactory proxyFactory,
IRuntimeHost runtimeHost,
SessionContext sessionContext,
int timeout_ms) : base(logger, processFactory, proxyFactory, runtimeHost, sessionContext, timeout_ms)
int timeout_ms) : base(dependencies, processFactory, proxyFactory, runtimeHost, timeout_ms)
{
}

View File

@@ -0,0 +1,128 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using SafeExamBrowser.Communication.Contracts.Data;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Configuration.Contracts.Cryptography;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Runtime.Communication;
using SafeExamBrowser.Settings;
using SafeExamBrowser.UserInterface.Contracts;
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal abstract class ConfigurationBaseOperation : SessionOperation
{
protected readonly IConfigurationRepository repository;
protected readonly IUserInterfaceFactory uiFactory;
protected string AppDataFilePath => Context.Next.AppConfig.AppDataFilePath;
protected string ProgramDataFilePath => Context.Next.AppConfig.ProgramDataFilePath;
public ConfigurationBaseOperation(
Dependencies dependencies,
IConfigurationRepository repository,
IUserInterfaceFactory uiFactory) : base(dependencies)
{
this.repository = repository;
this.uiFactory = uiFactory;
}
protected LoadStatus? TryLoadSettings(Uri uri, UriSource source, out PasswordParameters passwordParams, out AppSettings settings, string currentPassword = default)
{
passwordParams = new PasswordParameters { Password = string.Empty, IsHash = true };
var status = repository.TryLoadSettings(uri, out settings, passwordParams);
if (status == LoadStatus.PasswordNeeded && currentPassword != default)
{
passwordParams.Password = currentPassword;
passwordParams.IsHash = true;
status = repository.TryLoadSettings(uri, out settings, passwordParams);
}
for (var attempts = 0; attempts < 5 && status == LoadStatus.PasswordNeeded; attempts++)
{
var isLocalConfig = source == UriSource.AppData || source == UriSource.ProgramData;
var purpose = isLocalConfig ? PasswordRequestPurpose.LocalSettings : PasswordRequestPurpose.Settings;
var success = TryGetPassword(purpose, out var password);
if (success)
{
passwordParams.Password = password;
passwordParams.IsHash = false;
}
else
{
return null;
}
status = repository.TryLoadSettings(uri, out settings, passwordParams);
}
return status;
}
protected bool TryGetPassword(PasswordRequestPurpose purpose, out string password)
{
var success = false;
if (ClientBridge.IsRequired())
{
ClientBridge.TryGetPassword(purpose, out password);
}
else
{
var (message, title) = GetTextKeysFor(purpose);
var dialog = uiFactory.CreatePasswordDialog(Text.Get(message), Text.Get(title));
var result = dialog.Show(RuntimeWindow);
password = result.Password;
success = result.Success;
}
return success;
}
private (TextKey message, TextKey title) GetTextKeysFor(PasswordRequestPurpose purpose)
{
var message = default(TextKey);
var title = default(TextKey);
switch (purpose)
{
case PasswordRequestPurpose.LocalAdministrator:
message = TextKey.PasswordDialog_LocalAdminPasswordRequired;
title = TextKey.PasswordDialog_LocalAdminPasswordRequiredTitle;
break;
case PasswordRequestPurpose.LocalSettings:
message = TextKey.PasswordDialog_LocalSettingsPasswordRequired;
title = TextKey.PasswordDialog_LocalSettingsPasswordRequiredTitle;
break;
case PasswordRequestPurpose.Settings:
message = TextKey.PasswordDialog_SettingsPasswordRequired;
title = TextKey.PasswordDialog_SettingsPasswordRequiredTitle;
break;
}
return (message, title);
}
protected enum UriSource
{
Undefined,
AppData,
CommandLine,
ProgramData,
Reconfiguration,
Server
}
}
}

View File

@@ -7,6 +7,7 @@
*/
using System;
using System.Collections.Generic;
using System.IO;
using SafeExamBrowser.Communication.Contracts.Data;
using SafeExamBrowser.Configuration.Contracts;
@@ -14,39 +15,38 @@ using SafeExamBrowser.Configuration.Contracts.Cryptography;
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Runtime.Operations.Events;
using SafeExamBrowser.Settings;
using SafeExamBrowser.Settings.Security;
using SafeExamBrowser.SystemComponents.Contracts;
using SafeExamBrowser.UserInterface.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal class ConfigurationOperation : ConfigurationBaseOperation
{
private readonly string[] commandLineArgs;
private readonly IFileSystem fileSystem;
private readonly IHashAlgorithm hashAlgorithm;
private readonly ILogger logger;
public override event ActionRequiredEventHandler ActionRequired;
public override event StatusChangedEventHandler StatusChanged;
public ConfigurationOperation(
string[] commandLineArgs,
IConfigurationRepository configuration,
Dependencies dependencies,
IFileSystem fileSystem,
IHashAlgorithm hashAlgorithm,
ILogger logger,
SessionContext sessionContext) : base(commandLineArgs, configuration, sessionContext)
IConfigurationRepository repository,
IUserInterfaceFactory uiFactory) : base(dependencies, repository, uiFactory)
{
this.commandLineArgs = commandLineArgs;
this.fileSystem = fileSystem;
this.hashAlgorithm = hashAlgorithm;
this.logger = logger;
}
public override OperationResult Perform()
{
logger.Info("Initializing application configuration...");
Logger.Info("Initializing application configuration...");
StatusChanged?.Invoke(TextKey.OperationStatus_InitializeConfiguration);
var result = OperationResult.Failed;
@@ -68,7 +68,7 @@ namespace SafeExamBrowser.Runtime.Operations
public override OperationResult Repeat()
{
logger.Info("Initializing new application configuration...");
Logger.Info("Initializing new application configuration...");
StatusChanged?.Invoke(TextKey.OperationStatus_InitializeConfiguration);
var result = OperationResult.Failed;
@@ -80,7 +80,7 @@ namespace SafeExamBrowser.Runtime.Operations
}
else
{
logger.Warn($"The resource specified for reconfiguration does not exist or is not valid!");
Logger.Warn($"The resource specified for reconfiguration does not exist or is not valid!");
}
LogOperationResult(result);
@@ -93,15 +93,10 @@ namespace SafeExamBrowser.Runtime.Operations
return OperationResult.Success;
}
protected override void InvokeActionRequired(ActionRequiredEventArgs args)
{
ActionRequired?.Invoke(args);
}
private OperationResult LoadDefaultSettings()
{
logger.Info("No valid configuration resource specified and no local client configuration found - loading default settings...");
Context.Next.Settings = configuration.LoadDefaultSettings();
Logger.Info("No valid configuration resource specified and no local client configuration found - loading default settings...");
Context.Next.Settings = repository.LoadDefaultSettings();
return OperationResult.Success;
}
@@ -170,7 +165,7 @@ namespace SafeExamBrowser.Runtime.Operations
}
fileSystem.Delete(uri.LocalPath);
logger.Info($"Deleted temporary configuration file '{uri}'.");
Logger.Info($"Deleted temporary configuration file '{uri}'.");
return result;
}
@@ -213,17 +208,18 @@ namespace SafeExamBrowser.Runtime.Operations
Context.Next.IsBrowserResource = true;
Context.Next.Settings.Applications.Blacklist.Clear();
Context.Next.Settings.Applications.Whitelist.Clear();
Context.Next.Settings.Display.AllowedDisplays = 10;
Context.Next.Settings.Display.IgnoreError = true;
Context.Next.Settings.Display.InternalDisplayOnly = false;
Context.Next.Settings.Browser.DeleteCacheOnShutdown = false;
Context.Next.Settings.Browser.DeleteCookiesOnShutdown = false;
Context.Next.Settings.Browser.StartUrl = uri.AbsoluteUri;
Context.Next.Settings.Display.AllowedDisplays = 10;
Context.Next.Settings.Display.IgnoreError = true;
Context.Next.Settings.Display.InternalDisplayOnly = false;
Context.Next.Settings.Security.AllowReconfiguration = true;
Context.Next.Settings.Security.VirtualMachinePolicy = VirtualMachinePolicy.Allow;
Context.Next.Settings.Service.IgnoreService = true;
Context.Next.Settings.UserInterface.ActionCenter.EnableActionCenter = false;
logger.Info($"The configuration resource needs authentication or is a webpage, using '{uri}' as start URL for the browser.");
Logger.Info($"The configuration resource needs authentication or is a webpage, using '{uri}' as start URL for the browser.");
return OperationResult.Success;
}
@@ -249,7 +245,7 @@ namespace SafeExamBrowser.Runtime.Operations
private void HandleReconfigurationByBrowserResource()
{
Context.Next.Settings.Browser.DeleteCookiesOnStartup = false;
logger.Info("Some browser settings were overridden in order to retain a potential LMS / web application session.");
Logger.Info("Some browser settings were overridden in order to retain a potential LMS / web application session.");
}
private void HandleStartUrlQuery(Uri uri, UriSource source)
@@ -268,49 +264,34 @@ namespace SafeExamBrowser.Runtime.Operations
private bool? TryConfigureClient(Uri uri, PasswordParameters passwordParams, string currentPassword = default)
{
var mustAuthenticate = IsRequiredToAuthenticateForClientConfiguration(passwordParams, currentPassword);
var success = new bool?(true);
logger.Info("Starting client configuration...");
Logger.Info("Starting client configuration...");
if (mustAuthenticate)
{
var authenticated = AuthenticateForClientConfiguration(currentPassword);
if (authenticated == true)
{
logger.Info("Authentication was successful.");
}
if (authenticated == false)
{
logger.Info("Authentication has failed!");
ActionRequired?.Invoke(new InvalidPasswordMessageArgs());
return false;
}
if (!authenticated.HasValue)
{
logger.Info("Authentication was aborted.");
return null;
}
success = AuthenticateForClientConfiguration(currentPassword);
}
else
{
logger.Info("Authentication is not required.");
Logger.Info("Authentication is not required.");
}
var status = configuration.ConfigureClientWith(uri, passwordParams);
var success = status == SaveStatus.Success;
if (success == true)
{
var status = repository.ConfigureClientWith(uri, passwordParams);
if (success)
{
logger.Info("Client configuration was successful.");
}
else
{
logger.Error($"Client configuration failed with status '{status}'!");
ActionRequired?.Invoke(new ClientConfigurationErrorMessageArgs());
success = status == SaveStatus.Success;
if (success == true)
{
Logger.Info("Client configuration was successful.");
}
else
{
Logger.Error($"Client configuration failed with status '{status}'!");
ShowMessageBox(TextKey.MessageBox_ClientConfigurationError, TextKey.MessageBox_ClientConfigurationErrorTitle, icon: MessageBoxIcon.Error);
}
}
return success;
@@ -344,9 +325,9 @@ namespace SafeExamBrowser.Runtime.Operations
private bool? AuthenticateForClientConfiguration(string currentPassword)
{
var authenticated = false;
var authenticated = default(bool?);
for (var attempts = 0; attempts < 5 && !authenticated; attempts++)
for (var attempts = 0; attempts < 5 && authenticated != true; attempts++)
{
var success = TryGetPassword(PasswordRequestPurpose.LocalAdministrator, out var password);
@@ -356,40 +337,73 @@ namespace SafeExamBrowser.Runtime.Operations
}
else
{
return null;
authenticated = default;
break;
}
}
if (authenticated == true)
{
Logger.Info("Authentication was successful.");
}
if (authenticated == false)
{
Logger.Info("Authentication has failed!");
ShowMessageBox(TextKey.MessageBox_InvalidPasswordError, TextKey.MessageBox_InvalidPasswordErrorTitle, icon: MessageBoxIcon.Error);
}
if (authenticated == default)
{
Logger.Info("Authentication was aborted.");
}
return authenticated;
}
private bool AbortAfterClientConfiguration()
{
var args = new ConfigurationCompletedEventArgs();
var message = TextKey.MessageBox_ClientConfigurationQuestion;
var title = TextKey.MessageBox_ClientConfigurationQuestionTitle;
var result = ShowMessageBox(message, title, MessageBoxAction.YesNo, MessageBoxIcon.Question);
var abort = result == MessageBoxResult.Yes;
ActionRequired?.Invoke(args);
logger.Info($"The user chose to {(args.AbortStartup ? "abort" : "continue")} startup after successful client configuration.");
Logger.Info($"The user chose to {(abort ? "abort" : "continue")} startup after successful client configuration.");
return args.AbortStartup;
return abort;
}
private void ShowFailureMessage(LoadStatus status, Uri uri)
{
var error = MessageBoxIcon.Error;
var message = default(TextKey);
var placeholders = new Dictionary<string, string>();
var title = default(TextKey);
switch (status)
{
case LoadStatus.PasswordNeeded:
ActionRequired?.Invoke(new InvalidPasswordMessageArgs());
message = TextKey.MessageBox_InvalidPasswordError;
title = TextKey.MessageBox_InvalidPasswordErrorTitle;
break;
case LoadStatus.InvalidData:
ActionRequired?.Invoke(new InvalidDataMessageArgs(uri.ToString()));
message = TextKey.MessageBox_InvalidConfigurationData;
placeholders["%%URI%%"] = uri.ToString();
title = TextKey.MessageBox_InvalidConfigurationDataTitle;
break;
case LoadStatus.NotSupported:
ActionRequired?.Invoke(new NotSupportedMessageArgs(uri.ToString()));
message = TextKey.MessageBox_NotSupportedConfigurationResource;
placeholders["%%URI%%"] = uri.ToString();
title = TextKey.MessageBox_NotSupportedConfigurationResourceTitle;
break;
case LoadStatus.UnexpectedError:
ActionRequired?.Invoke(new UnexpectedErrorMessageArgs(uri.ToString()));
message = TextKey.MessageBox_UnexpectedConfigurationError;
placeholders["%%URI%%"] = uri.ToString();
title = TextKey.MessageBox_UnexpectedConfigurationErrorTitle;
break;
}
ShowMessageBox(message, title, icon: error, messagePlaceholders: placeholders);
}
private bool TryInitializeSettingsUri(out Uri uri, out UriSource source)
@@ -403,21 +417,21 @@ namespace SafeExamBrowser.Runtime.Operations
{
isValidUri = Uri.TryCreate(commandLineArgs[1], UriKind.Absolute, out uri);
source = UriSource.CommandLine;
logger.Info($"Found command-line argument for configuration resource: '{uri}', the URI is {(isValidUri ? "valid" : "invalid")}.");
Logger.Info($"Found command-line argument for configuration resource: '{uri}', the URI is {(isValidUri ? "valid" : "invalid")}.");
}
if (!isValidUri && File.Exists(ProgramDataFilePath))
{
isValidUri = Uri.TryCreate(ProgramDataFilePath, UriKind.Absolute, out uri);
source = UriSource.ProgramData;
logger.Info($"Found configuration file in program data directory: '{uri}'.");
Logger.Info($"Found configuration file in program data directory: '{uri}'.");
}
if (!isValidUri && File.Exists(AppDataFilePath))
{
isValidUri = Uri.TryCreate(AppDataFilePath, UriKind.Absolute, out uri);
source = UriSource.AppData;
logger.Info($"Found configuration file in app data directory: '{uri}'.");
Logger.Info($"Found configuration file in app data directory: '{uri}'.");
}
return isValidUri;
@@ -438,13 +452,13 @@ namespace SafeExamBrowser.Runtime.Operations
switch (result)
{
case OperationResult.Aborted:
logger.Info("The configuration was aborted by the user.");
Logger.Info("The configuration was aborted by the user.");
break;
case OperationResult.Failed:
logger.Warn("The configuration has failed!");
Logger.Warn("The configuration has failed!");
break;
case OperationResult.Success:
logger.Info("The configuration was successful.");
Logger.Info("The configuration was successful.");
break;
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Runtime.Communication;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
using SafeExamBrowser.UserInterface.Contracts.Windows;
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal class Dependencies
{
public ClientBridge ClientBridge { get; }
internal ILogger Logger { get; }
internal IMessageBox MessageBox { get; }
internal RuntimeContext RuntimeContext { get; }
internal IRuntimeWindow RuntimeWindow { get; }
internal IText Text { get; }
internal Dependencies(
ClientBridge clientBridge,
ILogger logger,
IMessageBox messageBox,
IRuntimeWindow runtimeWindow,
RuntimeContext runtimeContext,
IText text)
{
ClientBridge = clientBridge ?? throw new ArgumentNullException(nameof(clientBridge));
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
MessageBox = messageBox ?? throw new ArgumentNullException(nameof(messageBox));
RuntimeWindow = runtimeWindow ?? throw new ArgumentNullException(nameof(runtimeWindow));
RuntimeContext = runtimeContext ?? throw new ArgumentNullException(nameof(runtimeContext));
Text = text ?? throw new ArgumentNullException(nameof(text));
}
}
}

View File

@@ -9,22 +9,16 @@
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Runtime.Operations.Events;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal class DisclaimerOperation : SessionOperation
{
private readonly ILogger logger;
public override event ActionRequiredEventHandler ActionRequired;
public override event StatusChangedEventHandler StatusChanged;
public DisclaimerOperation(ILogger logger, SessionContext context) : base(context)
public DisclaimerOperation(Dependencies dependencies) : base(dependencies)
{
this.logger = logger;
}
public override OperationResult Perform()
@@ -58,29 +52,23 @@ namespace SafeExamBrowser.Runtime.Operations
private OperationResult ShowScreenProctoringDisclaimer()
{
var args = new MessageEventArgs
{
Action = MessageBoxAction.OkCancel,
Icon = MessageBoxIcon.Information,
Message = TextKey.MessageBox_ScreenProctoringDisclaimer,
Title = TextKey.MessageBox_ScreenProctoringDisclaimerTitle
};
StatusChanged?.Invoke(TextKey.OperationStatus_WaitDisclaimerConfirmation);
ActionRequired?.Invoke(args);
if (args.Result == MessageBoxResult.Ok)
var message = TextKey.MessageBox_ScreenProctoringDisclaimer;
var title = TextKey.MessageBox_ScreenProctoringDisclaimerTitle;
var result = ShowMessageBox(message, title, action: MessageBoxAction.OkCancel);
var operationResult = result == MessageBoxResult.Ok ? OperationResult.Success : OperationResult.Aborted;
if (result == MessageBoxResult.Ok)
{
logger.Info("The user confirmed the screen proctoring disclaimer.");
return OperationResult.Success;
Logger.Info("The user confirmed the screen proctoring disclaimer.");
}
else
{
logger.Warn("The user did not confirm the screen proctoring disclaimer! Aborting session initialization...");
return OperationResult.Aborted;
Logger.Warn("The user did not confirm the screen proctoring disclaimer! Aborting session initialization...");
}
return operationResult;
}
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Collections.Generic;
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Monitoring.Contracts.Display;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal class DisplayMonitorOperation : SessionOperation
{
private readonly IDisplayMonitor displayMonitor;
public override event StatusChangedEventHandler StatusChanged;
public DisplayMonitorOperation(Dependencies dependencies, IDisplayMonitor displayMonitor) : base(dependencies)
{
this.displayMonitor = displayMonitor;
}
public override OperationResult Perform()
{
return CheckDisplayConfiguration();
}
public override OperationResult Repeat()
{
return CheckDisplayConfiguration();
}
public override OperationResult Revert()
{
return OperationResult.Success;
}
private OperationResult CheckDisplayConfiguration()
{
Logger.Info("Validating display configuration...");
StatusChanged?.Invoke(TextKey.OperationStatus_ValidateDisplayConfiguration);
var validation = displayMonitor.ValidateConfiguration(Context.Next.Settings.Display);
var result = validation.IsAllowed ? OperationResult.Success : OperationResult.Failed;
if (validation.IsAllowed)
{
Logger.Info("Display configuration is allowed.");
}
else
{
Logger.Error("Display configuration is not allowed!");
ShowError(validation);
}
return result;
}
private void ShowError(ValidationResult validation)
{
var internalOnly = Text.Get(TextKey.MessageBox_DisplayConfigurationInternal);
var internalOrExternal = Text.Get(TextKey.MessageBox_DisplayConfigurationInternalOrExternal);
var message = TextKey.MessageBox_DisplayConfigurationError;
var title = TextKey.MessageBox_DisplayConfigurationErrorTitle;
var placeholders = new Dictionary<string, string>
{
{ "%%_ALLOWED_COUNT_%%", Convert.ToString(Context.Next.Settings.Display.AllowedDisplays) },
{ "%%_EXTERNAL_COUNT_%%", Convert.ToString(validation.ExternalDisplays) },
{ "%%_INTERNAL_COUNT_%%", Convert.ToString(validation.InternalDisplays) },
{ "%%_TYPE_%%", Context.Next.Settings.Display.InternalDisplayOnly ? internalOnly : internalOrExternal }
};
ShowMessageBox(message, title, icon: MessageBoxIcon.Error, messagePlaceholders: placeholders);
}
}
}

View File

@@ -9,45 +9,40 @@
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Settings.Security;
using SafeExamBrowser.WindowsApi.Contracts;
namespace SafeExamBrowser.Runtime.Operations
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal class KioskModeOperation : SessionOperation
{
private readonly IDesktopFactory desktopFactory;
private readonly IDesktopMonitor desktopMonitor;
private readonly IExplorerShell explorerShell;
private readonly ILogger logger;
private readonly IProcessFactory processFactory;
private KioskMode? activeMode;
private IDesktop customDesktop;
private IDesktop originalDesktop;
public override event ActionRequiredEventHandler ActionRequired { add { } remove { } }
public override event StatusChangedEventHandler StatusChanged;
public KioskModeOperation(
Dependencies dependencies,
IDesktopFactory desktopFactory,
IDesktopMonitor desktopMonitor,
IExplorerShell explorerShell,
ILogger logger,
IProcessFactory processFactory,
SessionContext sessionContext) : base(sessionContext)
IProcessFactory processFactory) : base(dependencies)
{
this.desktopFactory = desktopFactory;
this.desktopMonitor = desktopMonitor;
this.explorerShell = explorerShell;
this.logger = logger;
this.processFactory = processFactory;
}
public override OperationResult Perform()
{
logger.Info($"Initializing kiosk mode '{Context.Next.Settings.Security.KioskMode}'...");
Logger.Info($"Initializing kiosk mode '{Context.Next.Settings.Security.KioskMode}'...");
StatusChanged?.Invoke(TextKey.OperationStatus_InitializeKioskMode);
activeMode = Context.Next.Settings.Security.KioskMode;
@@ -71,11 +66,11 @@ namespace SafeExamBrowser.Runtime.Operations
if (activeMode == newMode)
{
logger.Info($"New kiosk mode '{newMode}' is the same as the currently active mode, skipping re-initialization...");
Logger.Info($"New kiosk mode '{newMode}' is the same as the currently active mode, skipping re-initialization...");
}
else
{
logger.Info($"Switching from kiosk mode '{activeMode}' to '{newMode}'...");
Logger.Info($"Switching from kiosk mode '{activeMode}' to '{newMode}'...");
StatusChanged?.Invoke(TextKey.OperationStatus_InitializeKioskMode);
switch (activeMode)
@@ -106,7 +101,7 @@ namespace SafeExamBrowser.Runtime.Operations
public override OperationResult Revert()
{
logger.Info($"Reverting kiosk mode '{activeMode}'...");
Logger.Info($"Reverting kiosk mode '{activeMode}'...");
StatusChanged?.Invoke(TextKey.OperationStatus_RevertKioskMode);
switch (activeMode)
@@ -125,14 +120,14 @@ namespace SafeExamBrowser.Runtime.Operations
private void CreateCustomDesktop()
{
originalDesktop = desktopFactory.GetCurrent();
logger.Info($"Current desktop is {originalDesktop}.");
Logger.Info($"Current desktop is {originalDesktop}.");
customDesktop = desktopFactory.CreateRandom();
logger.Info($"Created custom desktop {customDesktop}.");
Logger.Info($"Created custom desktop {customDesktop}.");
customDesktop.Activate();
processFactory.StartupDesktop = customDesktop;
logger.Info("Successfully activated custom desktop.");
Logger.Info("Successfully activated custom desktop.");
desktopMonitor.Start(customDesktop);
}
@@ -145,21 +140,21 @@ namespace SafeExamBrowser.Runtime.Operations
{
originalDesktop.Activate();
processFactory.StartupDesktop = originalDesktop;
logger.Info($"Switched back to original desktop {originalDesktop}.");
Logger.Info($"Switched back to original desktop {originalDesktop}.");
}
else
{
logger.Warn($"No original desktop found to activate!");
Logger.Warn($"No original desktop found to activate!");
}
if (customDesktop != default)
{
customDesktop.Close();
logger.Info($"Closed custom desktop {customDesktop}.");
Logger.Info($"Closed custom desktop {customDesktop}.");
}
else
{
logger.Warn($"No custom desktop found to close!");
Logger.Warn($"No custom desktop found to close!");
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* 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.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Monitoring.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal class RemoteSessionOperation : SessionOperation
{
private readonly IRemoteSessionDetector detector;
public override event StatusChangedEventHandler StatusChanged;
public RemoteSessionOperation(Dependencies dependencies, IRemoteSessionDetector detector) : base(dependencies)
{
this.detector = detector;
}
public override OperationResult Perform()
{
return ValidatePolicy();
}
public override OperationResult Repeat()
{
return ValidatePolicy();
}
public override OperationResult Revert()
{
return OperationResult.Success;
}
private OperationResult ValidatePolicy()
{
var result = OperationResult.Success;
Logger.Info($"Validating remote session policy...");
StatusChanged?.Invoke(TextKey.OperationStatus_ValidateRemoteSessionPolicy);
if (Context.Next.Settings.Service.DisableRemoteConnections && detector.IsRemoteSession())
{
result = OperationResult.Aborted;
Logger.Error("Detected remote session while SEB is not allowed to be run in a remote session! Aborting...");
ShowMessageBox(TextKey.MessageBox_RemoteSessionNotAllowed, TextKey.MessageBox_RemoteSessionNotAllowedTitle, icon: MessageBoxIcon.Error);
}
return result;
}
}
}

View File

@@ -13,35 +13,29 @@ using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Runtime.Operations.Events;
using SafeExamBrowser.Server.Contracts;
using SafeExamBrowser.Server.Contracts.Data;
using SafeExamBrowser.Settings;
using SafeExamBrowser.SystemComponents.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
using SafeExamBrowser.UserInterface.Contracts;
namespace SafeExamBrowser.Runtime.Operations
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal class ServerOperation : ConfigurationBaseOperation
{
private readonly IFileSystem fileSystem;
private readonly ILogger logger;
private readonly IServerProxy server;
public override event ActionRequiredEventHandler ActionRequired;
public override event StatusChangedEventHandler StatusChanged;
public ServerOperation(
string[] commandLineArgs,
IConfigurationRepository configuration,
Dependencies dependencies,
IFileSystem fileSystem,
ILogger logger,
SessionContext context,
IServerProxy server) : base(commandLineArgs, configuration, context)
IConfigurationRepository repository,
IServerProxy server,
IUserInterfaceFactory uiFactory) : base(dependencies, repository, uiFactory)
{
this.fileSystem = fileSystem;
this.logger = logger;
this.server = server;
}
@@ -56,7 +50,7 @@ namespace SafeExamBrowser.Runtime.Operations
var exams = default(IEnumerable<Exam>);
var uri = default(Uri);
logger.Info("Initializing server...");
Logger.Info("Initializing server...");
StatusChanged?.Invoke(TextKey.OperationStatus_InitializeServer);
server.Initialize(Context.Next.Settings.Server);
@@ -96,14 +90,23 @@ namespace SafeExamBrowser.Runtime.Operations
if (abort)
{
result = OperationResult.Aborted;
logger.Info("The user aborted the server operation.");
Logger.Info("The user aborted the server operation.");
}
if (fallback)
{
Context.Next.Settings.SessionMode = SessionMode.Normal;
result = OperationResult.Success;
logger.Info("The user chose to fallback and start a normal session.");
Logger.Info("The user chose to fallback and start a normal session.");
}
if (result == OperationResult.Success)
{
Logger.Info("Successfully initialized server.");
}
else if (result == OperationResult.Failed)
{
Logger.Error("Failed to initialize server!");
}
}
@@ -116,7 +119,16 @@ namespace SafeExamBrowser.Runtime.Operations
if (Context.Current.Settings.SessionMode == SessionMode.Server && Context.Next.Settings.SessionMode == SessionMode.Server)
{
result = AbortServerReconfiguration();
result = Revert();
if (result == OperationResult.Success)
{
result = Perform();
}
else
{
Logger.Error($"Cannot start new server session due to failed finalization of current server session! Terminating...");
}
}
else if (Context.Current.Settings.SessionMode == SessionMode.Server)
{
@@ -136,7 +148,7 @@ namespace SafeExamBrowser.Runtime.Operations
if (Context.Current?.Settings.SessionMode == SessionMode.Server || Context.Next?.Settings.SessionMode == SessionMode.Server)
{
logger.Info("Finalizing server...");
Logger.Info("Finalizing server...");
StatusChanged?.Invoke(TextKey.OperationStatus_FinalizeServer);
var disconnect = server.Disconnect();
@@ -144,21 +156,18 @@ namespace SafeExamBrowser.Runtime.Operations
if (disconnect.Success)
{
result = OperationResult.Success;
Logger.Info("Successfully finalized server.");
}
else
{
result = OperationResult.Failed;
Logger.Error("Failed to finalize server!");
}
}
return result;
}
protected override void InvokeActionRequired(ActionRequiredEventArgs args)
{
ActionRequired?.Invoke(args);
}
private OperationResult TryLoadServerSettings(Exam exam, Uri uri)
{
var info = server.GetConnectionInfo();
@@ -170,6 +179,7 @@ namespace SafeExamBrowser.Runtime.Operations
if (status == LoadStatus.Success)
{
var browserSettings = Context.Next.Settings.Browser;
var invigilationSettings = settings.Server.Invigilation;
var serverSettings = Context.Next.Settings.Server;
Context.Next.AppConfig.ServerApi = info.Api;
@@ -181,6 +191,7 @@ namespace SafeExamBrowser.Runtime.Operations
Context.Next.Settings.Browser.StartUrl = exam.Url;
Context.Next.Settings.Browser.StartUrlQuery = browserSettings.StartUrlQuery;
Context.Next.Settings.Server = serverSettings;
Context.Next.Settings.Server.Invigilation = invigilationSettings;
Context.Next.Settings.SessionMode = SessionMode.Server;
result = OperationResult.Success;
@@ -236,19 +247,33 @@ namespace SafeExamBrowser.Runtime.Operations
private bool Retry(string message, out bool abort, out bool fallback)
{
var args = new ServerFailureEventArgs(message, Context.Next.Settings.Server.PerformFallback);
AskForServerFailureAction(message, out abort, out fallback, out var retry);
ActionRequired?.Invoke(args);
abort = args.Abort;
fallback = args.Fallback;
if (args.Retry)
if (retry)
{
logger.Debug("The user chose to retry the current server request.");
Logger.Debug("The user chose to retry the current server request.");
}
return args.Retry;
return retry;
}
private void AskForServerFailureAction(string message, out bool abort, out bool fallback, out bool retry)
{
var showFallback = Context.Next.Settings.Server.PerformFallback;
if (ClientBridge.IsRequired())
{
ClientBridge.TryAskForServerFailureAction(message, showFallback, out abort, out fallback, out retry);
}
else
{
var dialog = uiFactory.CreateServerFailureDialog(message, showFallback);
var result = dialog.Show(RuntimeWindow);
abort = result.Abort;
fallback = result.Fallback;
retry = result.Retry;
}
}
private bool TrySelectExam(IEnumerable<Exam> exams, out Exam exam)
@@ -257,36 +282,35 @@ namespace SafeExamBrowser.Runtime.Operations
if (string.IsNullOrWhiteSpace(Context.Next.Settings.Server.ExamId))
{
var args = new ExamSelectionEventArgs(exams);
ActionRequired?.Invoke(args);
exam = args.SelectedExam;
success = args.Success;
success = TryAskForExamSelection(exams, out exam);
}
else
{
exam = exams.First();
logger.Info("Automatically selected exam as defined in configuration.");
Logger.Info("Automatically selected exam as defined in configuration.");
}
return success;
}
private OperationResult AbortServerReconfiguration()
private bool TryAskForExamSelection(IEnumerable<Exam> exams, out Exam exam)
{
var args = new MessageEventArgs
var success = false;
if (ClientBridge.IsRequired())
{
Action = MessageBoxAction.Ok,
Icon = MessageBoxIcon.Warning,
Message = TextKey.MessageBox_ServerReconfigurationWarning,
Title = TextKey.MessageBox_ServerReconfigurationWarningTitle
};
success = ClientBridge.TryAskForExamSelection(exams, out exam);
}
else
{
var dialog = uiFactory.CreateExamSelectionDialog(exams);
var result = dialog.Show(RuntimeWindow);
logger.Warn("Server reconfiguration is currently not supported, aborting...");
ActionRequired?.Invoke(args);
exam = result.SelectedExam;
success = result.Success;
}
return OperationResult.Aborted;
return success;
}
}
}

View File

@@ -15,50 +15,45 @@ using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Runtime.Operations.Events;
using SafeExamBrowser.Settings.Service;
using SafeExamBrowser.SystemComponents.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal class ServiceOperation : SessionOperation
{
private ILogger logger;
private IRuntimeHost runtimeHost;
private IServiceProxy service;
private readonly IRuntimeHost runtimeHost;
private readonly IServiceProxy serviceProxy;
private readonly int timeout_ms;
private readonly IUserInfo userInfo;
private string serviceEventName;
private Guid? sessionId;
private int timeout_ms;
private IUserInfo userInfo;
public override event ActionRequiredEventHandler ActionRequired;
public override event StatusChangedEventHandler StatusChanged;
public ServiceOperation(
ILogger logger,
Dependencies dependencies,
IRuntimeHost runtimeHost,
IServiceProxy service,
SessionContext sessionContext,
IServiceProxy serviceProxy,
int timeout_ms,
IUserInfo userInfo) : base(sessionContext)
IUserInfo userInfo) : base(dependencies)
{
this.logger = logger;
this.runtimeHost = runtimeHost;
this.service = service;
this.serviceProxy = serviceProxy;
this.timeout_ms = timeout_ms;
this.userInfo = userInfo;
}
public override OperationResult Perform()
{
logger.Info($"Initializing service...");
Logger.Info($"Initializing service...");
StatusChanged?.Invoke(TextKey.OperationStatus_InitializeServiceSession);
var success = IgnoreService() || TryInitializeConnection();
if (success && service.IsConnected)
if (success && serviceProxy.IsConnected)
{
success = TryStartSession();
}
@@ -68,12 +63,12 @@ namespace SafeExamBrowser.Runtime.Operations
public override OperationResult Repeat()
{
logger.Info($"Initializing service...");
Logger.Info($"Initializing service...");
StatusChanged?.Invoke(TextKey.OperationStatus_InitializeServiceSession);
var success = true;
if (service.IsConnected)
if (serviceProxy.IsConnected)
{
if (sessionId.HasValue)
{
@@ -90,7 +85,7 @@ namespace SafeExamBrowser.Runtime.Operations
success = IgnoreService() || TryInitializeConnection();
}
if (success && service.IsConnected)
if (success && serviceProxy.IsConnected)
{
success = TryStartSession();
}
@@ -100,12 +95,12 @@ namespace SafeExamBrowser.Runtime.Operations
public override OperationResult Revert()
{
logger.Info("Finalizing service...");
Logger.Info("Finalizing service...");
StatusChanged?.Invoke(TextKey.OperationStatus_FinalizeServiceSession);
var success = true;
if (service.IsConnected)
if (serviceProxy.IsConnected)
{
if (sessionId.HasValue)
{
@@ -122,7 +117,7 @@ namespace SafeExamBrowser.Runtime.Operations
{
if (Context.Next.Settings.Service.IgnoreService)
{
logger.Info("The service will be ignored for the next session.");
Logger.Info("The service will be ignored for the next session.");
return true;
}
@@ -134,32 +129,22 @@ namespace SafeExamBrowser.Runtime.Operations
{
var mandatory = Context.Next.Settings.Service.Policy == ServicePolicy.Mandatory;
var warn = Context.Next.Settings.Service.Policy == ServicePolicy.Warn;
var connected = service.Connect();
var connected = serviceProxy.Connect();
var success = connected || !mandatory;
if (success)
{
logger.Info($"The service is {(mandatory ? "mandatory" : "optional")} and {(connected ? "connected." : "not connected.")}");
Logger.Info($"The service is {(mandatory ? "mandatory" : "optional")} and {(connected ? "connected." : "not connected.")}");
if (!connected && warn)
{
ActionRequired?.Invoke(new MessageEventArgs
{
Icon = MessageBoxIcon.Warning,
Message = TextKey.MessageBox_ServiceUnavailableWarning,
Title = TextKey.MessageBox_ServiceUnavailableWarningTitle
});
ShowMessageBox(TextKey.MessageBox_ServiceUnavailableWarning, TextKey.MessageBox_ServiceUnavailableWarningTitle, icon: MessageBoxIcon.Warning);
}
}
else
{
logger.Error("The service is mandatory but no connection could be established!");
ActionRequired?.Invoke(new MessageEventArgs
{
Icon = MessageBoxIcon.Error,
Message = TextKey.MessageBox_ServiceUnavailableError,
Title = TextKey.MessageBox_ServiceUnavailableErrorTitle
});
Logger.Error("The service is mandatory but no connection could be established!");
ShowMessageBox(TextKey.MessageBox_ServiceUnavailableError, TextKey.MessageBox_ServiceUnavailableErrorTitle, icon: MessageBoxIcon.Error);
}
return success;
@@ -167,15 +152,15 @@ namespace SafeExamBrowser.Runtime.Operations
private bool TryTerminateConnection()
{
var disconnected = service.Disconnect();
var disconnected = serviceProxy.Disconnect();
if (disconnected)
{
logger.Info("Successfully disconnected from service.");
Logger.Info("Successfully disconnected from service.");
}
else
{
logger.Error("Failed to disconnect from service!");
Logger.Error("Failed to disconnect from service!");
}
return disconnected;
@@ -193,9 +178,9 @@ namespace SafeExamBrowser.Runtime.Operations
};
var started = false;
logger.Info("Starting new service session...");
Logger.Info("Starting new service session...");
var communication = service.StartSession(configuration);
var communication = serviceProxy.StartSession(configuration);
if (communication.Success)
{
@@ -205,16 +190,16 @@ namespace SafeExamBrowser.Runtime.Operations
{
sessionId = Context.Next.SessionId;
serviceEventName = Context.Next.AppConfig.ServiceEventName;
logger.Info("Successfully started new service session.");
Logger.Info("Successfully started new service session.");
}
else
{
logger.Error($"Failed to start new service session within {timeout_ms / 1000} seconds!");
Logger.Error($"Failed to start new service session within {timeout_ms / 1000} seconds!");
}
}
else
{
logger.Error("Failed to communicate session start command to service!");
Logger.Error("Failed to communicate session start command to service!");
}
return started;
@@ -224,9 +209,9 @@ namespace SafeExamBrowser.Runtime.Operations
{
var success = false;
logger.Info("Stopping current service session...");
Logger.Info("Stopping current service session...");
var communication = service.StopSession(sessionId.Value);
var communication = serviceProxy.StopSession(sessionId.Value);
if (communication.Success)
{
@@ -234,32 +219,32 @@ namespace SafeExamBrowser.Runtime.Operations
if (success)
{
sessionId = default(Guid?);
serviceEventName = default(string);
logger.Info("Successfully stopped service session.");
sessionId = default;
serviceEventName = default;
Logger.Info("Successfully stopped service session.");
}
else
{
logger.Error($"Failed to stop service session within {timeout_ms / 1000} seconds!");
Logger.Error($"Failed to stop service session within {timeout_ms / 1000} seconds!");
}
}
else
{
logger.Error("Failed to communicate session stop command to service!");
Logger.Error("Failed to communicate session stop command to service!");
}
if (success && isFinalSession)
{
communication = service.RunSystemConfigurationUpdate();
communication = serviceProxy.RunSystemConfigurationUpdate();
success = communication.Success;
if (communication.Success)
{
logger.Info("Instructed service to perform system configuration update.");
Logger.Info("Instructed service to perform system configuration update.");
}
else
{
logger.Error("Failed to communicate system configuration update command to service!");
Logger.Error("Failed to communicate system configuration update command to service!");
}
}

View File

@@ -8,20 +8,15 @@
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.Logging.Contracts;
namespace SafeExamBrowser.Runtime.Operations
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal class SessionActivationOperation : SessionOperation
{
private ILogger logger;
public override event ActionRequiredEventHandler ActionRequired { add { } remove { } }
public override event StatusChangedEventHandler StatusChanged { add { } remove { } }
public SessionActivationOperation(ILogger logger, SessionContext sessionContext) : base(sessionContext)
public SessionActivationOperation(Dependencies dependencies) : base(dependencies)
{
this.logger = logger;
}
public override OperationResult Perform()
@@ -47,31 +42,31 @@ namespace SafeExamBrowser.Runtime.Operations
private void SwitchLogSeverity()
{
if (logger.LogLevel != Context.Next.Settings.LogLevel)
if (Logger.LogLevel != Context.Next.Settings.LogLevel)
{
var current = logger.LogLevel.ToString().ToUpper();
var current = Logger.LogLevel.ToString().ToUpper();
var next = Context.Next.Settings.LogLevel.ToString().ToUpper();
logger.Info($"Switching from log severity '{current}' to '{next}' for new session.");
logger.LogLevel = Context.Next.Settings.LogLevel;
Logger.Info($"Switching from log severity '{current}' to '{next}' for new session.");
Logger.LogLevel = Context.Next.Settings.LogLevel;
}
}
private void ActivateNewSession()
{
var isFirstSession = Context.Current == null;
var isFirstSession = Context.Current == default;
if (isFirstSession)
{
logger.Info($"Successfully activated first session '{Context.Next.SessionId}'.");
Logger.Info($"Successfully activated first session '{Context.Next.SessionId}'.");
}
else
{
logger.Info($"Successfully terminated old session '{Context.Current.SessionId}' and activated new session '{Context.Next.SessionId}'.");
Logger.Info($"Successfully terminated old session '{Context.Current.SessionId}' and activated new session '{Context.Next.SessionId}'.");
}
Context.Current = Context.Next;
Context.Next = null;
Context.Next = default;
}
}
}

View File

@@ -6,37 +6,28 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using SafeExamBrowser.Communication.Contracts.Hosts;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.SystemComponents.Contracts;
namespace SafeExamBrowser.Runtime.Operations
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal class SessionInitializationOperation : SessionOperation
{
private IConfigurationRepository configuration;
private IFileSystem fileSystem;
private ILogger logger;
private IRuntimeHost runtimeHost;
private readonly IConfigurationRepository repository;
private readonly IFileSystem fileSystem;
public override event ActionRequiredEventHandler ActionRequired { add { } remove { } }
public override event StatusChangedEventHandler StatusChanged;
public SessionInitializationOperation(
IConfigurationRepository configuration,
Dependencies dependencies,
IFileSystem fileSystem,
ILogger logger,
IRuntimeHost runtimeHost,
SessionContext sessionContext) : base(sessionContext)
IConfigurationRepository repository) : base(dependencies)
{
this.configuration = configuration;
this.fileSystem = fileSystem;
this.logger = logger;
this.runtimeHost = runtimeHost;
this.repository = repository;
}
public override OperationResult Perform()
@@ -62,14 +53,14 @@ namespace SafeExamBrowser.Runtime.Operations
private void InitializeSessionConfiguration()
{
logger.Info("Initializing new session configuration...");
Logger.Info("Initializing new session configuration...");
StatusChanged?.Invoke(TextKey.OperationStatus_InitializeSession);
Context.Next = configuration.InitializeSessionConfiguration();
Context.Next = repository.InitializeSessionConfiguration();
logger.Info($" -> Client-ID: {Context.Next.AppConfig.ClientId}");
logger.Info($" -> Runtime-ID: {Context.Next.AppConfig.RuntimeId}");
logger.Info($" -> Session-ID: {Context.Next.SessionId}");
Logger.Info($" -> Client-ID: {Context.Next.AppConfig.ClientId}");
Logger.Info($" -> Runtime-ID: {Context.Next.AppConfig.RuntimeId}");
Logger.Info($" -> Session-ID: {Context.Next.SessionId}");
fileSystem.CreateDirectory(Context.Next.AppConfig.TemporaryDirectory);
}

View File

@@ -9,22 +9,18 @@
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Monitoring.Contracts.System;
namespace SafeExamBrowser.Runtime.Operations
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal class SessionIntegrityOperation : SessionOperation
{
private readonly ILogger logger;
private readonly ISystemSentinel sentinel;
public override event ActionRequiredEventHandler ActionRequired { add { } remove { } }
public override event StatusChangedEventHandler StatusChanged;
public SessionIntegrityOperation(ILogger logger, ISystemSentinel sentinel, SessionContext context) : base(context)
public SessionIntegrityOperation(Dependencies dependencies, ISystemSentinel sentinel) : base(dependencies)
{
this.logger = logger;
this.sentinel = sentinel;
}
@@ -88,11 +84,11 @@ namespace SafeExamBrowser.Runtime.Operations
{
if (success)
{
logger.Info("Successfully ensured session integrity.");
Logger.Info("Successfully ensured session integrity.");
}
else
{
logger.Error("Failed to ensure session integrity! Aborting session initialization...");
Logger.Error("Failed to ensure session integrity! Aborting session initialization...");
}
}
@@ -106,7 +102,7 @@ namespace SafeExamBrowser.Runtime.Operations
}
else
{
logger.Debug("Verification of cursor configuration is disabled.");
Logger.Debug("Verification of cursor configuration is disabled.");
}
return success;
@@ -120,12 +116,12 @@ namespace SafeExamBrowser.Runtime.Operations
{
if (Context.Current?.Settings.Service.IgnoreService == false)
{
logger.Info($"Ease of access configuration is compromised but service was active in the current session.");
Logger.Info($"Ease of access configuration is compromised but service was active in the current session.");
success = true;
}
else if (!Context.Next.Settings.Service.IgnoreService)
{
logger.Info($"Ease of access configuration is compromised but service will be active in the next session.");
Logger.Info($"Ease of access configuration is compromised but service will be active in the next session.");
success = true;
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System.Collections.Generic;
using System.Linq;
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Runtime.Communication;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
using SafeExamBrowser.UserInterface.Contracts.Windows;
namespace SafeExamBrowser.Runtime.Operations.Session
{
/// <summary>
/// The base implementation to be used for all operations in the session operation sequence.
/// </summary>
internal abstract class SessionOperation : IRepeatableOperation
{
private readonly IMessageBox messageBox;
protected ClientBridge ClientBridge { get; }
protected RuntimeContext Context { get; }
protected ILogger Logger { get; }
protected IRuntimeWindow RuntimeWindow { get; }
protected IText Text { get; }
public abstract event StatusChangedEventHandler StatusChanged;
internal SessionOperation(Dependencies dependencies)
{
ClientBridge = dependencies.ClientBridge;
Context = dependencies.RuntimeContext;
Logger = dependencies.Logger;
messageBox = dependencies.MessageBox;
RuntimeWindow = dependencies.RuntimeWindow;
Text = dependencies.Text;
}
public abstract OperationResult Perform();
public abstract OperationResult Repeat();
public abstract OperationResult Revert();
/// <summary>
/// Shows a message box either directly or (if required) via the currently running client application component. All session operations
/// should always use this method instead of using <see cref="IMessageBox"/> directly!
/// </summary>
protected MessageBoxResult ShowMessageBox(
TextKey messageKey,
TextKey titleKey,
MessageBoxAction action = MessageBoxAction.Ok,
MessageBoxIcon icon = MessageBoxIcon.Information,
IDictionary<string, string> messagePlaceholders = default,
IDictionary<string, string> titlePlaceholders = default)
{
var message = Text.Get(messageKey);
var result = default(MessageBoxResult);
var title = Text.Get(titleKey);
foreach (var placeholder in messagePlaceholders ?? Enumerable.Empty<KeyValuePair<string, string>>())
{
message = message.Replace(placeholder.Key, placeholder.Value);
}
foreach (var placeholder in titlePlaceholders ?? Enumerable.Empty<KeyValuePair<string, string>>())
{
title = title.Replace(placeholder.Key, placeholder.Value);
}
if (ClientBridge.IsRequired())
{
result = ClientBridge.ShowMessageBox(message, title, action, icon);
}
else
{
result = messageBox.Show(message, title, action, icon, RuntimeWindow);
}
return result;
}
}
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System.Collections.Generic;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.Core.OperationModel;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.UserInterface.Contracts.Windows;
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal class SessionOperationSequence : RepeatableOperationSequence<SessionOperation>
{
private readonly IRuntimeWindow runtimeWindow;
internal SessionOperationSequence(ILogger logger, IEnumerable<SessionOperation> operations, IRuntimeWindow runtimeWindow) : base(logger, operations)
{
this.runtimeWindow = runtimeWindow;
ProgressChanged += SessionSequence_ProgressChanged;
StatusChanged += SessionSequence_StatusChanged;
}
private void SessionSequence_ProgressChanged(ProgressChangedEventArgs args)
{
if (args.CurrentValue.HasValue)
{
runtimeWindow?.SetValue(args.CurrentValue.Value);
}
if (args.IsIndeterminate == true)
{
runtimeWindow?.SetIndeterminate();
}
if (args.MaxValue.HasValue)
{
runtimeWindow?.SetMaxValue(args.MaxValue.Value);
}
if (args.Progress == true)
{
runtimeWindow?.Progress();
}
if (args.Regress == true)
{
runtimeWindow?.Regress();
}
}
private void SessionSequence_StatusChanged(TextKey status)
{
runtimeWindow?.UpdateStatus(status, true);
}
}
}

View File

@@ -12,26 +12,19 @@ using System.Text;
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Runtime.Operations.Events;
using SafeExamBrowser.Settings.Security;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal class VersionRestrictionOperation : SessionOperation
{
private readonly ILogger logger;
private readonly IText text;
private IList<VersionRestriction> Restrictions => Context.Next.Settings.Security.VersionRestrictions;
public override event ActionRequiredEventHandler ActionRequired;
public override event StatusChangedEventHandler StatusChanged;
public VersionRestrictionOperation(ILogger logger, SessionContext context, IText text) : base(context)
public VersionRestrictionOperation(Dependencies dependencies) : base(dependencies)
{
this.logger = logger;
this.text = text;
}
public override OperationResult Perform()
@@ -53,7 +46,7 @@ namespace SafeExamBrowser.Runtime.Operations
{
var result = OperationResult.Success;
logger.Info("Validating version restrictions...");
Logger.Info("Validating version restrictions...");
StatusChanged?.Invoke(TextKey.OperationStatus_ValidateVersionRestrictions);
if (Restrictions.Any())
@@ -63,19 +56,18 @@ namespace SafeExamBrowser.Runtime.Operations
if (Restrictions.Any(r => IsFulfilled(r)))
{
logger.Info($"The installed SEB version '{version}' complies with the version restrictions: {requiredVersions}.");
Logger.Info($"The installed SEB version '{version}' complies with the version restrictions: {requiredVersions}.");
}
else
{
result = OperationResult.Aborted;
logger.Error($"The installed SEB version '{version}' does not comply with the version restrictions: {requiredVersions}.");
ActionRequired?.Invoke(new VersionRestrictionMessageArgs(version, BuildRequiredVersions()));
Logger.Error($"The installed SEB version '{version}' does not comply with the version restrictions: {requiredVersions}.");
ShowErrorMessage(version);
}
}
else
{
logger.Info($"There are no version restrictions for the configuration.");
Logger.Info($"There are no version restrictions for the configuration.");
}
return result;
@@ -83,40 +75,40 @@ namespace SafeExamBrowser.Runtime.Operations
private bool IsFulfilled(VersionRestriction restriction)
{
var isFulfilled = true;
var fulfilled = true;
var (major, minor, patch, build, isAllianceEdition) = GetVersion();
if (restriction.IsMinimumRestriction)
{
isFulfilled &= restriction.Major <= major;
fulfilled &= restriction.Major <= major;
if (restriction.Major == major)
{
isFulfilled &= restriction.Minor <= minor;
fulfilled &= restriction.Minor <= minor;
if (restriction.Minor == minor)
{
isFulfilled &= !restriction.Patch.HasValue || restriction.Patch <= patch;
fulfilled &= !restriction.Patch.HasValue || restriction.Patch <= patch;
if (restriction.Patch == patch)
{
isFulfilled &= !restriction.Build.HasValue || restriction.Build <= build;
fulfilled &= !restriction.Build.HasValue || restriction.Build <= build;
}
}
}
isFulfilled &= !restriction.RequiresAllianceEdition || isAllianceEdition;
fulfilled &= !restriction.RequiresAllianceEdition || isAllianceEdition;
}
else
{
isFulfilled &= restriction.Major == major;
isFulfilled &= restriction.Minor == minor;
isFulfilled &= !restriction.Patch.HasValue || restriction.Patch == patch;
isFulfilled &= !restriction.Build.HasValue || restriction.Build == build;
isFulfilled &= !restriction.RequiresAllianceEdition || isAllianceEdition;
fulfilled &= restriction.Major == major;
fulfilled &= restriction.Minor == minor;
fulfilled &= !restriction.Patch.HasValue || restriction.Patch == patch;
fulfilled &= !restriction.Build.HasValue || restriction.Build == build;
fulfilled &= !restriction.RequiresAllianceEdition || isAllianceEdition;
}
return isFulfilled;
return fulfilled;
}
private (int major, int minor, int patch, int build, bool isAllianceEdition) GetVersion()
@@ -134,7 +126,7 @@ namespace SafeExamBrowser.Runtime.Operations
private string BuildRequiredVersions()
{
var info = new StringBuilder();
var minimumVersionText = text.Get(TextKey.MessageBox_VersionRestrictionMinimum);
var minimumVersionText = Text.Get(TextKey.MessageBox_VersionRestrictionMinimum);
info.AppendLine();
info.AppendLine();
@@ -160,5 +152,18 @@ namespace SafeExamBrowser.Runtime.Operations
return info.ToString();
}
private void ShowErrorMessage(string version)
{
var message = TextKey.MessageBox_VersionRestrictionError;
var title = TextKey.MessageBox_VersionRestrictionErrorTitle;
var placeholders = new Dictionary<string, string>()
{
{"%%_VERSION_%%", version },
{ "%%_REQUIRED_VERSIONS_%%", BuildRequiredVersions() }
};
ShowMessageBox(message, title, icon: MessageBoxIcon.Error, messagePlaceholders: placeholders);
}
}
}

View File

@@ -9,26 +9,21 @@
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Monitoring.Contracts;
using SafeExamBrowser.Runtime.Operations.Events;
using SafeExamBrowser.Settings.Security;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
namespace SafeExamBrowser.Runtime.Operations
namespace SafeExamBrowser.Runtime.Operations.Session
{
internal class VirtualMachineOperation : SessionOperation
{
private readonly IVirtualMachineDetector detector;
private readonly ILogger logger;
public override event ActionRequiredEventHandler ActionRequired;
public override event StatusChangedEventHandler StatusChanged;
public VirtualMachineOperation(IVirtualMachineDetector detector, ILogger logger, SessionContext context) : base(context)
public VirtualMachineOperation(Dependencies dependencies, IVirtualMachineDetector detector) : base(dependencies)
{
this.detector = detector;
this.logger = logger;
}
public override OperationResult Perform()
@@ -48,25 +43,19 @@ namespace SafeExamBrowser.Runtime.Operations
private OperationResult ValidatePolicy()
{
logger.Info($"Validating virtual machine policy...");
var result = OperationResult.Success;
Logger.Info($"Validating virtual machine policy...");
StatusChanged?.Invoke(TextKey.OperationStatus_ValidateVirtualMachinePolicy);
if (Context.Next.Settings.Security.VirtualMachinePolicy == VirtualMachinePolicy.Deny && detector.IsVirtualMachine())
{
var args = new MessageEventArgs
{
Icon = MessageBoxIcon.Error,
Message = TextKey.MessageBox_VirtualMachineNotAllowed,
Title = TextKey.MessageBox_VirtualMachineNotAllowedTitle
};
logger.Error("Detected virtual machine while SEB is not allowed to be run in a virtual machine! Aborting...");
ActionRequired?.Invoke(args);
return OperationResult.Aborted;
result = OperationResult.Aborted;
Logger.Error("Detected virtual machine while SEB is not allowed to be run in a virtual machine! Aborting...");
ShowMessageBox(TextKey.MessageBox_VirtualMachineNotAllowed, TextKey.MessageBox_VirtualMachineNotAllowedTitle, icon: MessageBoxIcon.Error);
}
return OperationResult.Success;
return result;
}
}
}

View File

@@ -1,33 +0,0 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* 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.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
namespace SafeExamBrowser.Runtime.Operations
{
/// <summary>
/// The base implementation to be used for all operations in the session operation sequence.
/// </summary>
internal abstract class SessionOperation : IRepeatableOperation
{
protected SessionContext Context { get; private set; }
public abstract event ActionRequiredEventHandler ActionRequired;
public abstract event StatusChangedEventHandler StatusChanged;
public SessionOperation(SessionContext context)
{
Context = context;
}
public abstract OperationResult Perform();
public abstract OperationResult Repeat();
public abstract OperationResult Revert();
}
}

View File

@@ -16,6 +16,9 @@ using System.Windows;
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// Required for mocking internal contracts with Moq
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
[assembly: InternalsVisibleTo("SafeExamBrowser.Runtime.UnitTests")]
//In order to begin building localizable applications, set
@@ -48,6 +51,6 @@ using System.Windows;
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("3.9.0.787")]
[assembly: AssemblyFileVersion("3.9.0.787")]
[assembly: AssemblyInformationalVersion("3.9.0.787")]
[assembly: AssemblyVersion("3.10.0.826")]
[assembly: AssemblyFileVersion("3.10.0.826")]
[assembly: AssemblyInformationalVersion("3.10.0.826")]

View File

@@ -0,0 +1,93 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
using SafeExamBrowser.UserInterface.Contracts.Windows;
namespace SafeExamBrowser.Runtime.Responsibilities
{
internal class ClientResponsibility : RuntimeResponsibility
{
private readonly IMessageBox messageBox;
private readonly IRuntimeWindow runtimeWindow;
private readonly Action shutdown;
internal ClientResponsibility(
ILogger logger,
IMessageBox messageBox,
RuntimeContext runtimeContext,
IRuntimeWindow runtimeWindow,
Action shutdown) : base(logger, runtimeContext)
{
this.messageBox = messageBox;
this.runtimeWindow = runtimeWindow;
this.shutdown = shutdown;
}
public override void Assume(RuntimeTask task)
{
switch (task)
{
case RuntimeTask.DeregisterSessionEvents:
DeregisterEvents();
break;
case RuntimeTask.RegisterSessionEvents:
RegisterEvents();
break;
}
}
private void DeregisterEvents()
{
if (Context.ClientProcess != default)
{
Context.ClientProcess.Terminated -= ClientProcess_Terminated;
}
if (Context.ClientProxy != default)
{
Context.ClientProxy.ConnectionLost -= ClientProxy_ConnectionLost;
}
}
private void RegisterEvents()
{
Context.ClientProcess.Terminated += ClientProcess_Terminated;
Context.ClientProxy.ConnectionLost += ClientProxy_ConnectionLost;
}
private void ClientProcess_Terminated(int exitCode)
{
Logger.Error($"Client application has unexpectedly terminated with exit code {exitCode}!");
if (SessionIsRunning)
{
Context.Responsibilities.Delegate(RuntimeTask.StopSession);
}
//messageBox.Show(TextKey.MessageBox_ApplicationError, TextKey.MessageBox_ApplicationErrorTitle, icon: MessageBoxIcon.Error, parent: runtimeWindow);
shutdown.Invoke();
}
private void ClientProxy_ConnectionLost()
{
Logger.Error("Lost connection to the client application!");
if (SessionIsRunning)
{
Context.Responsibilities.Delegate(RuntimeTask.StopSession);
}
messageBox.Show(TextKey.MessageBox_ApplicationError, TextKey.MessageBox_ApplicationErrorTitle, icon: MessageBoxIcon.Error, parent: runtimeWindow);
shutdown.Invoke();
}
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using SafeExamBrowser.Communication.Contracts.Events;
using SafeExamBrowser.Communication.Contracts.Hosts;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Logging.Contracts;
namespace SafeExamBrowser.Runtime.Responsibilities
{
internal class CommunicationResponsibility : RuntimeResponsibility
{
private readonly IRuntimeHost runtimeHost;
private readonly Action shutdown;
internal CommunicationResponsibility(
ILogger logger,
RuntimeContext runtimeContext,
IRuntimeHost runtimeHost,
Action shutdown) : base(logger, runtimeContext)
{
this.runtimeHost = runtimeHost;
this.shutdown = shutdown;
}
public override void Assume(RuntimeTask task)
{
switch (task)
{
case RuntimeTask.DeregisterEvents:
DeregisterEvents();
break;
case RuntimeTask.RegisterEvents:
RegisterEvents();
break;
}
}
private void RegisterEvents()
{
runtimeHost.ClientConfigurationNeeded += RuntimeHost_ClientConfigurationNeeded;
runtimeHost.ReconfigurationRequested += RuntimeHost_ReconfigurationRequested;
runtimeHost.ShutdownRequested += RuntimeHost_ShutdownRequested;
}
private void DeregisterEvents()
{
runtimeHost.ClientConfigurationNeeded -= RuntimeHost_ClientConfigurationNeeded;
runtimeHost.ReconfigurationRequested -= RuntimeHost_ReconfigurationRequested;
runtimeHost.ShutdownRequested -= RuntimeHost_ShutdownRequested;
}
private void RuntimeHost_ClientConfigurationNeeded(ClientConfigurationEventArgs args)
{
args.ClientConfiguration = new ClientConfiguration
{
AppConfig = Context.Next.AppConfig,
SessionId = Context.Next.SessionId,
Settings = Context.Next.Settings
};
}
private void RuntimeHost_ReconfigurationRequested(ReconfigurationEventArgs args)
{
Logger.Info($"Accepted request for reconfiguration with '{args.ConfigurationPath}'.");
Context.ReconfigurationFilePath = args.ConfigurationPath;
Context.ReconfigurationUrl = args.ResourceUrl;
Context.Responsibilities.Delegate(RuntimeTask.StartSession);
}
private void RuntimeHost_ShutdownRequested()
{
Logger.Info("Received shutdown request from the client application.");
shutdown.Invoke();
}
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* 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.Configuration.Contracts;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
using SafeExamBrowser.UserInterface.Contracts.Windows;
namespace SafeExamBrowser.Runtime.Responsibilities
{
internal class ErrorMessageResponsibility : RuntimeResponsibility
{
private readonly AppConfig appConfig;
private readonly IMessageBox messageBox;
private readonly ISplashScreen splashScreen;
private readonly IText text;
internal ErrorMessageResponsibility(
AppConfig appConfig,
ILogger logger,
IMessageBox messageBox,
RuntimeContext runtimeContext,
ISplashScreen splashScreen,
IText text) : base(logger, runtimeContext)
{
this.appConfig = appConfig;
this.messageBox = messageBox;
this.splashScreen = splashScreen;
this.text = text;
}
public override void Assume(RuntimeTask task)
{
switch (task)
{
case RuntimeTask.ShowShutdownError:
ShowShutdownErrorMessage();
break;
case RuntimeTask.ShowStartupError:
ShowStartupErrorMessage();
break;
}
}
private void ShowShutdownErrorMessage()
{
var message = AppendLogFilePaths(appConfig, text.Get(TextKey.MessageBox_ShutdownError));
var title = text.Get(TextKey.MessageBox_ShutdownErrorTitle);
messageBox.Show(message, title, icon: MessageBoxIcon.Error, parent: splashScreen);
}
private void ShowStartupErrorMessage()
{
var message = AppendLogFilePaths(appConfig, text.Get(TextKey.MessageBox_StartupError));
var title = text.Get(TextKey.MessageBox_StartupErrorTitle);
messageBox.Show(message, title, icon: MessageBoxIcon.Error, parent: splashScreen);
}
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.IO;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Core.Contracts.ResponsibilityModel;
using SafeExamBrowser.Logging.Contracts;
namespace SafeExamBrowser.Runtime.Responsibilities
{
internal abstract class RuntimeResponsibility : IResponsibility<RuntimeTask>
{
protected RuntimeContext Context { get; private set; }
protected ILogger Logger { get; private set; }
protected SessionConfiguration Session => Context.Current;
protected bool SessionIsRunning => Session != default;
internal RuntimeResponsibility(ILogger logger, RuntimeContext runtimeContext)
{
Logger = logger;
Context = runtimeContext;
}
public abstract void Assume(RuntimeTask task);
protected string AppendLogFilePaths(AppConfig appConfig, string message)
{
if (File.Exists(appConfig.BrowserLogFilePath))
{
message += $"{Environment.NewLine}{Environment.NewLine}{appConfig.BrowserLogFilePath}";
}
if (File.Exists(appConfig.ClientLogFilePath))
{
message += $"{Environment.NewLine}{Environment.NewLine}{appConfig.ClientLogFilePath}";
}
if (File.Exists(appConfig.RuntimeLogFilePath))
{
message += $"{Environment.NewLine}{Environment.NewLine}{appConfig.RuntimeLogFilePath}";
}
if (File.Exists(appConfig.ServiceLogFilePath))
{
message += $"{Environment.NewLine}{Environment.NewLine}{appConfig.ServiceLogFilePath}";
}
return message;
}
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* 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.Runtime.Responsibilities
{
/// <summary>
/// Defines all tasks assumed by the responsibilities of the runtime application.
/// </summary>
internal enum RuntimeTask
{
/// <summary>
/// Deregisters the respective event handlers during application termination.
/// </summary>
DeregisterEvents,
/// <summary>
/// Deregisters the respective event handlers during session termination.
/// </summary>
DeregisterSessionEvents,
/// <summary>
/// Registers the respective event handlers during application initialization.
/// </summary>
RegisterEvents,
/// <summary>
/// Registers the respective event handlers during session initialization.
/// </summary>
RegisterSessionEvents,
/// <summary>
/// Shows an error message in case the application shutdown fails.
/// </summary>
ShowShutdownError,
/// <summary>
/// Shows an error message in case the application startup fails.
/// </summary>
ShowStartupError,
/// <summary>
/// Attempts to start a new session.
/// </summary>
StartSession,
/// <summary>
/// Stops the currently running session.
/// </summary>
StopSession
}
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using SafeExamBrowser.Communication.Contracts.Proxies;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Settings.Service;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
using SafeExamBrowser.UserInterface.Contracts.Windows;
namespace SafeExamBrowser.Runtime.Responsibilities
{
internal class ServiceResponsibility : RuntimeResponsibility
{
private readonly IMessageBox messageBox;
private readonly IRuntimeWindow runtimeWindow;
private readonly IServiceProxy serviceProxy;
private readonly Action shutdown;
internal ServiceResponsibility(
ILogger logger,
IMessageBox messageBox,
RuntimeContext runtimeContext,
IRuntimeWindow runtimeWindow,
IServiceProxy serviceProxy,
Action shutdown) : base(logger, runtimeContext)
{
this.messageBox = messageBox;
this.runtimeWindow = runtimeWindow;
this.serviceProxy = serviceProxy;
this.shutdown = shutdown;
}
public override void Assume(RuntimeTask task)
{
switch (task)
{
case RuntimeTask.DeregisterSessionEvents:
DeregisterEvents();
break;
case RuntimeTask.RegisterSessionEvents:
RegisterEvents();
break;
}
}
private void DeregisterEvents()
{
serviceProxy.ConnectionLost -= ServiceProxy_ConnectionLost;
}
private void RegisterEvents()
{
serviceProxy.ConnectionLost += ServiceProxy_ConnectionLost;
}
private void ServiceProxy_ConnectionLost()
{
if (SessionIsRunning && Session.Settings.Service.Policy == ServicePolicy.Mandatory)
{
Logger.Error("Lost connection to the service component!");
Context.Responsibilities.Delegate(RuntimeTask.StopSession);
messageBox.Show(TextKey.MessageBox_ApplicationError, TextKey.MessageBox_ApplicationErrorTitle, icon: MessageBoxIcon.Error, parent: runtimeWindow);
shutdown.Invoke();
}
else
{
Logger.Warn("Lost connection to the service component!");
}
}
}
}

View File

@@ -0,0 +1,181 @@
/*
* Copyright (c) 2025 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Settings.Security;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
using SafeExamBrowser.UserInterface.Contracts.Windows;
namespace SafeExamBrowser.Runtime.Responsibilities
{
internal class SessionResponsibility : RuntimeResponsibility
{
private readonly AppConfig appConfig;
private readonly IMessageBox messageBox;
private readonly IRuntimeWindow runtimeWindow;
private readonly IRepeatableOperationSequence sessionSequence;
private readonly Action shutdown;
private readonly IText text;
internal SessionResponsibility(
AppConfig appConfig,
ILogger logger,
IMessageBox messageBox,
RuntimeContext runtimeContext,
IRuntimeWindow runtimeWindow,
IRepeatableOperationSequence sessionSequence,
Action shutdown,
IText text) : base(logger, runtimeContext)
{
this.appConfig = appConfig;
this.messageBox = messageBox;
this.runtimeWindow = runtimeWindow;
this.sessionSequence = sessionSequence;
this.shutdown = shutdown;
this.text = text;
}
public override void Assume(RuntimeTask task)
{
switch (task)
{
case RuntimeTask.StartSession:
StartSession();
break;
case RuntimeTask.StopSession:
StopSession();
break;
}
}
private void StartSession()
{
runtimeWindow.Show();
runtimeWindow.BringToForeground();
runtimeWindow.ShowProgressBar = true;
Logger.Info(AppendDivider("Session Start Procedure"));
if (SessionIsRunning)
{
Context.Responsibilities.Delegate(RuntimeTask.DeregisterSessionEvents);
}
var result = SessionIsRunning ? sessionSequence.TryRepeat() : sessionSequence.TryPerform();
if (result == OperationResult.Success)
{
Logger.Info(AppendDivider("Session Running"));
HandleSessionStartSuccess();
}
else if (result == OperationResult.Failed)
{
Logger.Info(AppendDivider("Session Start Failed"));
HandleSessionStartFailure();
}
else if (result == OperationResult.Aborted)
{
Logger.Info(AppendDivider("Session Start Aborted"));
HandleSessionStartAbortion();
}
}
private void StopSession()
{
runtimeWindow.Show();
runtimeWindow.BringToForeground();
runtimeWindow.ShowProgressBar = true;
Logger.Info(AppendDivider("Session Stop Procedure"));
Context.Responsibilities.Delegate(RuntimeTask.DeregisterSessionEvents);
var success = sessionSequence.TryRevert() == OperationResult.Success;
if (success)
{
Logger.Info(AppendDivider("Session Terminated"));
}
else
{
Logger.Info(AppendDivider("Session Stop Failed"));
}
}
private void HandleSessionStartSuccess()
{
Context.Responsibilities.Delegate(RuntimeTask.RegisterSessionEvents);
runtimeWindow.ShowProgressBar = false;
runtimeWindow.ShowLog = Session.Settings.Security.AllowApplicationLogAccess;
runtimeWindow.TopMost = Session.Settings.Security.KioskMode != KioskMode.None;
runtimeWindow.UpdateStatus(TextKey.RuntimeWindow_ApplicationRunning);
//if (Session.Settings.Security.KioskMode == KioskMode.DisableExplorerShell)
//{
// runtimeWindow.Hide();
//}
runtimeWindow.Hide();
}
private void HandleSessionStartFailure()
{
var message = AppendLogFilePaths(appConfig, text.Get(TextKey.MessageBox_SessionStartError));
var title = text.Get(TextKey.MessageBox_SessionStartErrorTitle);
if (SessionIsRunning)
{
StopSession();
messageBox.Show(message, title, icon: MessageBoxIcon.Error, parent: runtimeWindow);
Logger.Info("Terminating application...");
shutdown.Invoke();
}
else
{
messageBox.Show(message, title, icon: MessageBoxIcon.Error, parent: runtimeWindow);
}
}
private void HandleSessionStartAbortion()
{
if (SessionIsRunning)
{
Context.Responsibilities.Delegate(RuntimeTask.RegisterSessionEvents);
runtimeWindow.ShowProgressBar = false;
runtimeWindow.UpdateStatus(TextKey.RuntimeWindow_ApplicationRunning);
runtimeWindow.TopMost = Session.Settings.Security.KioskMode != KioskMode.None;
//if (Session.Settings.Security.KioskMode == KioskMode.DisableExplorerShell)
//{
// runtimeWindow.Hide();
//}
runtimeWindow.Hide();
Context.ClientProxy.InformReconfigurationAborted();
}
}
private string AppendDivider(string message)
{
var dashesLeft = new string('-', 48 - message.Length / 2 - message.Length % 2);
var dashesRight = new string('-', 48 - message.Length / 2);
return $"### {dashesLeft} {message} {dashesRight} ###";
}
}
}

View File

@@ -8,14 +8,16 @@
using SafeExamBrowser.Communication.Contracts.Proxies;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Core.Contracts.ResponsibilityModel;
using SafeExamBrowser.Runtime.Responsibilities;
using SafeExamBrowser.WindowsApi.Contracts;
namespace SafeExamBrowser.Runtime
{
/// <summary>
/// Holds all configuration and runtime data required for the session handling.
/// Holds all configuration and session data for the runtime.
/// </summary>
internal class SessionContext
internal class RuntimeContext
{
/// <summary>
/// The currently running client process.
@@ -46,5 +48,10 @@ namespace SafeExamBrowser.Runtime
/// The original URL from where the configuration file was downloaded.
/// </summary>
internal string ReconfigurationUrl { get; set; }
/// <summary>
/// The runtime responsibilities.
/// </summary>
internal IResponsibilityCollection<RuntimeTask> Responsibilities { get; set; }
}
}

View File

@@ -6,98 +6,48 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.IO;
using System.Linq;
using System.Threading;
using SafeExamBrowser.Communication.Contracts.Data;
using SafeExamBrowser.Communication.Contracts.Events;
using SafeExamBrowser.Communication.Contracts.Hosts;
using SafeExamBrowser.Communication.Contracts.Proxies;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Core.Contracts.ResponsibilityModel;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Runtime.Operations.Events;
using SafeExamBrowser.Server.Contracts.Data;
using SafeExamBrowser.Settings.Security;
using SafeExamBrowser.Settings.Service;
using SafeExamBrowser.UserInterface.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
using SafeExamBrowser.Runtime.Responsibilities;
using SafeExamBrowser.UserInterface.Contracts.Windows;
using System.Deployment.Internal;
namespace SafeExamBrowser.Runtime
{
internal class RuntimeController
{
private readonly AppConfig appConfig;
private readonly ILogger logger;
private readonly IMessageBox messageBox;
private readonly IOperationSequence bootstrapSequence;
private readonly IRepeatableOperationSequence sessionSequence;
private readonly IRuntimeHost runtimeHost;
private readonly IResponsibilityCollection<RuntimeTask> responsibilities;
private readonly RuntimeContext runtimeContext;
private readonly IRuntimeWindow runtimeWindow;
private readonly IServiceProxy service;
private readonly SessionContext sessionContext;
private readonly ISplashScreen splashScreen;
private readonly Action shutdown;
private readonly IText text;
private readonly IUserInterfaceFactory uiFactory;
private SessionConfiguration Session
{
get { return sessionContext.Current; }
}
private bool SessionIsRunning
{
get { return Session != null; }
}
private bool SessionIsRunning => runtimeContext.Current != default;
internal RuntimeController(
AppConfig appConfig,
ILogger logger,
IMessageBox messageBox,
IOperationSequence bootstrapSequence,
IRepeatableOperationSequence sessionSequence,
IRuntimeHost runtimeHost,
IResponsibilityCollection<RuntimeTask> responsibilities,
RuntimeContext runtimeContext,
IRuntimeWindow runtimeWindow,
IServiceProxy service,
SessionContext sessionContext,
Action shutdown,
ISplashScreen splashScreen,
IText text,
IUserInterfaceFactory uiFactory)
ISplashScreen splashScreen)
{
this.appConfig = appConfig;
this.bootstrapSequence = bootstrapSequence;
this.responsibilities = responsibilities;
this.logger = logger;
this.messageBox = messageBox;
this.runtimeHost = runtimeHost;
this.runtimeWindow = runtimeWindow;
this.sessionSequence = sessionSequence;
this.service = service;
this.sessionContext = sessionContext;
this.shutdown = shutdown;
this.runtimeContext = runtimeContext;
this.splashScreen = splashScreen;
this.text = text;
this.uiFactory = uiFactory;
}
internal bool TryStart()
{
logger.Info("Initiating startup procedure...");
bootstrapSequence.ProgressChanged += BootstrapSequence_ProgressChanged;
bootstrapSequence.StatusChanged += BootstrapSequence_StatusChanged;
sessionSequence.ActionRequired += SessionSequence_ActionRequired;
sessionSequence.ProgressChanged += SessionSequence_ProgressChanged;
sessionSequence.StatusChanged += SessionSequence_StatusChanged;
// We need to show the runtime window here already, this way implicitly setting it as the runtime application's main window.
// Otherwise, the splash screen is considered as the main window and thus the operating system and/or WPF does not correctly
// activate the runtime window once bootstrapping has finished, which in turn leads to undesired UI behavior.
// activate the runtime window once bootstrapping has finished, which in turn leads to undesired user interface behavior.
runtimeWindow.Show();
runtimeWindow.BringToForeground();
runtimeWindow.SetIndeterminate();
@@ -109,21 +59,21 @@ namespace SafeExamBrowser.Runtime
if (initialized)
{
RegisterEvents();
responsibilities.Delegate(RuntimeTask.RegisterEvents);
splashScreen.Hide();
logger.Info("Application successfully initialized.");
logger.Log(string.Empty);
logger.Subscribe(runtimeWindow);
splashScreen.Hide();
StartSession();
responsibilities.Delegate(RuntimeTask.StartSession);
}
else
{
logger.Info("Application startup aborted!");
logger.Log(string.Empty);
messageBox.Show(AppendLogFilePaths(TextKey.MessageBox_StartupError), text.Get(TextKey.MessageBox_StartupErrorTitle), icon: MessageBoxIcon.Error, parent: splashScreen);
responsibilities.Delegate(RuntimeTask.ShowStartupError);
}
return initialized && SessionIsRunning;
@@ -131,11 +81,11 @@ namespace SafeExamBrowser.Runtime
internal void Terminate()
{
DeregisterEvents();
responsibilities.Delegate(RuntimeTask.DeregisterEvents);
if (SessionIsRunning)
{
StopSession();
responsibilities.Delegate(RuntimeTask.StopSession);
}
logger.Unsubscribe(runtimeWindow);
@@ -159,586 +109,10 @@ namespace SafeExamBrowser.Runtime
logger.Info("Shutdown procedure failed!");
logger.Log(string.Empty);
messageBox.Show(AppendLogFilePaths(TextKey.MessageBox_ShutdownError), text.Get(TextKey.MessageBox_ShutdownErrorTitle), icon: MessageBoxIcon.Error, parent: splashScreen);
responsibilities.Delegate(RuntimeTask.ShowShutdownError);
}
splashScreen.Close();
}
private void StartSession()
{
runtimeWindow.Show();
runtimeWindow.BringToForeground();
runtimeWindow.ShowProgressBar = true;
logger.Info(AppendDivider("Session Start Procedure"));
if (SessionIsRunning)
{
DeregisterSessionEvents();
}
var result = SessionIsRunning ? sessionSequence.TryRepeat() : sessionSequence.TryPerform();
if (result == OperationResult.Success)
{
logger.Info(AppendDivider("Session Running"));
HandleSessionStartSuccess();
}
else if (result == OperationResult.Failed)
{
logger.Info(AppendDivider("Session Start Failed"));
HandleSessionStartFailure();
}
else if (result == OperationResult.Aborted)
{
logger.Info(AppendDivider("Session Start Aborted"));
HandleSessionStartAbortion();
}
}
private void HandleSessionStartSuccess()
{
RegisterSessionEvents();
runtimeWindow.ShowProgressBar = false;
runtimeWindow.ShowLog = Session.Settings.Security.AllowApplicationLogAccess;
runtimeWindow.TopMost = Session.Settings.Security.KioskMode != KioskMode.None;
runtimeWindow.UpdateStatus(TextKey.RuntimeWindow_ApplicationRunning);
//if (Session.Settings.Security.KioskMode == KioskMode.DisableExplorerShell)
//{
// runtimeWindow.Hide();
//}
runtimeWindow.Hide();
}
private void HandleSessionStartFailure()
{
if (SessionIsRunning)
{
StopSession();
messageBox.Show(AppendLogFilePaths(TextKey.MessageBox_SessionStartError), text.Get(TextKey.MessageBox_SessionStartErrorTitle), icon: MessageBoxIcon.Error, parent: runtimeWindow);
logger.Info("Terminating application...");
shutdown.Invoke();
}
else
{
messageBox.Show(AppendLogFilePaths(TextKey.MessageBox_SessionStartError), text.Get(TextKey.MessageBox_SessionStartErrorTitle), icon: MessageBoxIcon.Error, parent: runtimeWindow);
}
}
private void HandleSessionStartAbortion()
{
if (SessionIsRunning)
{
RegisterSessionEvents();
runtimeWindow.ShowProgressBar = false;
runtimeWindow.UpdateStatus(TextKey.RuntimeWindow_ApplicationRunning);
runtimeWindow.TopMost = Session.Settings.Security.KioskMode != KioskMode.None;
//if (Session.Settings.Security.KioskMode == KioskMode.DisableExplorerShell)
//{
// runtimeWindow.Hide();
//}
runtimeWindow.Hide();
sessionContext.ClientProxy.InformReconfigurationAborted();
}
}
private void StopSession()
{
runtimeWindow.Show();
runtimeWindow.BringToForeground();
runtimeWindow.ShowProgressBar = true;
logger.Info(AppendDivider("Session Stop Procedure"));
DeregisterSessionEvents();
var success = sessionSequence.TryRevert() == OperationResult.Success;
if (success)
{
logger.Info(AppendDivider("Session Terminated"));
}
else
{
logger.Info(AppendDivider("Session Stop Failed"));
}
}
private void RegisterEvents()
{
runtimeHost.ClientConfigurationNeeded += RuntimeHost_ClientConfigurationNeeded;
runtimeHost.ReconfigurationRequested += RuntimeHost_ReconfigurationRequested;
runtimeHost.ShutdownRequested += RuntimeHost_ShutdownRequested;
}
private void DeregisterEvents()
{
runtimeHost.ClientConfigurationNeeded -= RuntimeHost_ClientConfigurationNeeded;
runtimeHost.ReconfigurationRequested -= RuntimeHost_ReconfigurationRequested;
runtimeHost.ShutdownRequested -= RuntimeHost_ShutdownRequested;
}
private void RegisterSessionEvents()
{
service.ConnectionLost += ServiceProxy_ConnectionLost;
sessionContext.ClientProcess.Terminated += ClientProcess_Terminated;
sessionContext.ClientProxy.ConnectionLost += ClientProxy_ConnectionLost;
}
private void DeregisterSessionEvents()
{
service.ConnectionLost -= ServiceProxy_ConnectionLost;
if (sessionContext.ClientProcess != null)
{
sessionContext.ClientProcess.Terminated -= ClientProcess_Terminated;
}
if (sessionContext.ClientProxy != null)
{
sessionContext.ClientProxy.ConnectionLost -= ClientProxy_ConnectionLost;
}
}
private void BootstrapSequence_ProgressChanged(ProgressChangedEventArgs args)
{
MapProgress(splashScreen, args);
}
private void BootstrapSequence_StatusChanged(TextKey status)
{
splashScreen.UpdateStatus(status, true);
}
private void ClientProcess_Terminated(int exitCode)
{
logger.Error($"Client application has unexpectedly terminated with exit code {exitCode}!");
if (SessionIsRunning)
{
StopSession();
}
//messageBox.Show(TextKey.MessageBox_ApplicationError, TextKey.MessageBox_ApplicationErrorTitle, icon: MessageBoxIcon.Error, parent: runtimeWindow);
shutdown.Invoke();
}
private void ClientProxy_ConnectionLost()
{
logger.Error("Lost connection to the client application!");
if (SessionIsRunning)
{
StopSession();
}
messageBox.Show(TextKey.MessageBox_ApplicationError, TextKey.MessageBox_ApplicationErrorTitle, icon: MessageBoxIcon.Error, parent: runtimeWindow);
shutdown.Invoke();
}
private void RuntimeHost_ClientConfigurationNeeded(ClientConfigurationEventArgs args)
{
args.ClientConfiguration = new ClientConfiguration
{
AppConfig = sessionContext.Next.AppConfig,
SessionId = sessionContext.Next.SessionId,
Settings = sessionContext.Next.Settings
};
}
private void RuntimeHost_ReconfigurationRequested(ReconfigurationEventArgs args)
{
logger.Info($"Accepted request for reconfiguration with '{args.ConfigurationPath}'.");
sessionContext.ReconfigurationFilePath = args.ConfigurationPath;
sessionContext.ReconfigurationUrl = args.ResourceUrl;
StartSession();
}
private void RuntimeHost_ShutdownRequested()
{
logger.Info("Received shutdown request from the client application.");
shutdown.Invoke();
}
private void ServiceProxy_ConnectionLost()
{
if (SessionIsRunning && Session.Settings.Service.Policy == ServicePolicy.Mandatory)
{
logger.Error("Lost connection to the service component!");
StopSession();
messageBox.Show(TextKey.MessageBox_ApplicationError, TextKey.MessageBox_ApplicationErrorTitle, icon: MessageBoxIcon.Error, parent: runtimeWindow);
shutdown.Invoke();
}
else
{
logger.Warn("Lost connection to the service component!");
}
}
private void SessionSequence_ActionRequired(ActionRequiredEventArgs args)
{
switch (args)
{
case ConfigurationCompletedEventArgs a:
AskIfConfigurationSufficient(a);
break;
case ExamSelectionEventArgs a:
AskForExamSelection(a);
break;
case MessageEventArgs m:
ShowMessageBox(m);
break;
case PasswordRequiredEventArgs p:
AskForPassword(p);
break;
case ServerFailureEventArgs a:
AskForServerFailureAction(a);
break;
}
}
private void AskForExamSelection(ExamSelectionEventArgs args)
{
var isStartup = !SessionIsRunning;
var isRunningOnDefaultDesktop = SessionIsRunning && Session.Settings.Security.KioskMode == KioskMode.DisableExplorerShell;
if (isStartup || isRunningOnDefaultDesktop)
{
TryAskForExamSelectionViaDialog(args);
}
else
{
TryAskForExamSelectionViaClient(args);
}
}
private void AskForServerFailureAction(ServerFailureEventArgs args)
{
var isStartup = !SessionIsRunning;
var isRunningOnDefaultDesktop = SessionIsRunning && Session.Settings.Security.KioskMode == KioskMode.DisableExplorerShell;
if (isStartup || isRunningOnDefaultDesktop)
{
TryAskForServerFailureActionViaDialog(args);
}
else
{
TryAskForServerFailureActionViaClient(args);
}
}
private void AskIfConfigurationSufficient(ConfigurationCompletedEventArgs args)
{
var message = TextKey.MessageBox_ClientConfigurationQuestion;
var title = TextKey.MessageBox_ClientConfigurationQuestionTitle;
var result = messageBox.Show(message, title, MessageBoxAction.YesNo, MessageBoxIcon.Question, runtimeWindow);
args.AbortStartup = result == MessageBoxResult.Yes;
}
private void AskForPassword(PasswordRequiredEventArgs args)
{
var isStartup = !SessionIsRunning;
var isRunningOnDefaultDesktop = SessionIsRunning && Session.Settings.Security.KioskMode == KioskMode.DisableExplorerShell;
if (isStartup || isRunningOnDefaultDesktop)
{
TryGetPasswordViaDialog(args);
}
else
{
TryGetPasswordViaClient(args);
}
}
private void ShowMessageBox(MessageEventArgs args)
{
var isStartup = !SessionIsRunning;
var isRunningOnDefaultDesktop = SessionIsRunning && Session.Settings.Security.KioskMode == KioskMode.DisableExplorerShell;
var message = text.Get(args.Message);
var title = text.Get(args.Title);
foreach (var placeholder in args.MessagePlaceholders)
{
message = message.Replace(placeholder.Key, placeholder.Value);
}
foreach (var placeholder in args.TitlePlaceholders)
{
title = title.Replace(placeholder.Key, placeholder.Value);
}
if (isStartup || isRunningOnDefaultDesktop)
{
args.Result = messageBox.Show(message, title, args.Action, args.Icon, runtimeWindow);
}
else
{
args.Result = ShowMessageBoxViaClient(message, title, args.Action, args.Icon);
}
}
private MessageBoxResult ShowMessageBoxViaClient(string message, string title, MessageBoxAction action, MessageBoxIcon icon)
{
var requestId = Guid.NewGuid();
var result = MessageBoxResult.None;
var response = default(MessageBoxReplyEventArgs);
var responseEvent = new AutoResetEvent(false);
var responseEventHandler = new CommunicationEventHandler<MessageBoxReplyEventArgs>((args) =>
{
if (args.RequestId == requestId)
{
response = args;
responseEvent.Set();
}
});
runtimeHost.MessageBoxReplyReceived += responseEventHandler;
var communication = sessionContext.ClientProxy.ShowMessage(message, title, (int) action, (int) icon, requestId);
if (communication.Success)
{
responseEvent.WaitOne();
result = (MessageBoxResult) response.Result;
}
runtimeHost.MessageBoxReplyReceived -= responseEventHandler;
return result;
}
private void TryAskForExamSelectionViaDialog(ExamSelectionEventArgs args)
{
var dialog = uiFactory.CreateExamSelectionDialog(args.Exams);
var result = dialog.Show(runtimeWindow);
args.SelectedExam = result.SelectedExam;
args.Success = result.Success;
}
private void TryAskForExamSelectionViaClient(ExamSelectionEventArgs args)
{
var exams = args.Exams.Select(e => (e.Id, e.LmsName, e.Name, e.Url));
var requestId = Guid.NewGuid();
var response = default(ExamSelectionReplyEventArgs);
var responseEvent = new AutoResetEvent(false);
var responseEventHandler = new CommunicationEventHandler<ExamSelectionReplyEventArgs>((a) =>
{
if (a.RequestId == requestId)
{
response = a;
responseEvent.Set();
}
});
runtimeHost.ExamSelectionReceived += responseEventHandler;
var communication = sessionContext.ClientProxy.RequestExamSelection(exams, requestId);
if (communication.Success)
{
responseEvent.WaitOne();
args.SelectedExam = args.Exams.First(e => e.Id == response.SelectedExamId);
args.Success = response.Success;
}
else
{
args.SelectedExam = default(Exam);
args.Success = false;
}
runtimeHost.ExamSelectionReceived -= responseEventHandler;
}
private void TryAskForServerFailureActionViaDialog(ServerFailureEventArgs args)
{
var dialog = uiFactory.CreateServerFailureDialog(args.Message, args.ShowFallback);
var result = dialog.Show(runtimeWindow);
args.Abort = result.Abort;
args.Fallback = result.Fallback;
args.Retry = result.Retry;
}
private void TryAskForServerFailureActionViaClient(ServerFailureEventArgs args)
{
var requestId = Guid.NewGuid();
var response = default(ServerFailureActionReplyEventArgs);
var responseEvent = new AutoResetEvent(false);
var responseEventHandler = new CommunicationEventHandler<ServerFailureActionReplyEventArgs>((a) =>
{
if (a.RequestId == requestId)
{
response = a;
responseEvent.Set();
}
});
runtimeHost.ServerFailureActionReceived += responseEventHandler;
var communication = sessionContext.ClientProxy.RequestServerFailureAction(args.Message, args.ShowFallback, requestId);
if (communication.Success)
{
responseEvent.WaitOne();
args.Abort = response.Abort;
args.Fallback = response.Fallback;
args.Retry = response.Retry;
}
else
{
args.Abort = true;
args.Fallback = false;
args.Retry = false;
}
runtimeHost.ServerFailureActionReceived -= responseEventHandler;
}
private void TryGetPasswordViaDialog(PasswordRequiredEventArgs args)
{
var message = default(TextKey);
var title = default(TextKey);
switch (args.Purpose)
{
case PasswordRequestPurpose.LocalAdministrator:
message = TextKey.PasswordDialog_LocalAdminPasswordRequired;
title = TextKey.PasswordDialog_LocalAdminPasswordRequiredTitle;
break;
case PasswordRequestPurpose.LocalSettings:
message = TextKey.PasswordDialog_LocalSettingsPasswordRequired;
title = TextKey.PasswordDialog_LocalSettingsPasswordRequiredTitle;
break;
case PasswordRequestPurpose.Settings:
message = TextKey.PasswordDialog_SettingsPasswordRequired;
title = TextKey.PasswordDialog_SettingsPasswordRequiredTitle;
break;
}
var dialog = uiFactory.CreatePasswordDialog(text.Get(message), text.Get(title));
var result = dialog.Show(runtimeWindow);
args.Password = result.Password;
args.Success = result.Success;
}
private void TryGetPasswordViaClient(PasswordRequiredEventArgs args)
{
var requestId = Guid.NewGuid();
var response = default(PasswordReplyEventArgs);
var responseEvent = new AutoResetEvent(false);
var responseEventHandler = new CommunicationEventHandler<PasswordReplyEventArgs>((a) =>
{
if (a.RequestId == requestId)
{
response = a;
responseEvent.Set();
}
});
runtimeHost.PasswordReceived += responseEventHandler;
var communication = sessionContext.ClientProxy.RequestPassword(args.Purpose, requestId);
if (communication.Success)
{
responseEvent.WaitOne();
args.Password = response.Password;
args.Success = response.Success;
}
else
{
args.Password = default(string);
args.Success = false;
}
runtimeHost.PasswordReceived -= responseEventHandler;
}
private void SessionSequence_ProgressChanged(ProgressChangedEventArgs args)
{
MapProgress(runtimeWindow, args);
}
private void SessionSequence_StatusChanged(TextKey status)
{
runtimeWindow?.UpdateStatus(status, true);
}
private void MapProgress(IProgressIndicator progressIndicator, ProgressChangedEventArgs args)
{
if (args.CurrentValue.HasValue)
{
progressIndicator?.SetValue(args.CurrentValue.Value);
}
if (args.IsIndeterminate == true)
{
progressIndicator?.SetIndeterminate();
}
if (args.MaxValue.HasValue)
{
progressIndicator?.SetMaxValue(args.MaxValue.Value);
}
if (args.Progress == true)
{
progressIndicator?.Progress();
}
if (args.Regress == true)
{
progressIndicator?.Regress();
}
}
private string AppendDivider(string message)
{
var dashesLeft = new String('-', 48 - message.Length / 2 - message.Length % 2);
var dashesRight = new String('-', 48 - message.Length / 2);
return $"### {dashesLeft} {message} {dashesRight} ###";
}
private string AppendLogFilePaths(TextKey key)
{
var message = text.Get(key);
if (File.Exists(appConfig.BrowserLogFilePath))
{
message += $"{Environment.NewLine}{Environment.NewLine}{appConfig.BrowserLogFilePath}";
}
if (File.Exists(appConfig.ClientLogFilePath))
{
message += $"{Environment.NewLine}{Environment.NewLine}{appConfig.ClientLogFilePath}";
}
if (File.Exists(appConfig.RuntimeLogFilePath))
{
message += $"{Environment.NewLine}{Environment.NewLine}{appConfig.RuntimeLogFilePath}";
}
if (File.Exists(appConfig.ServiceLogFilePath))
{
message += $"{Environment.NewLine}{Environment.NewLine}{appConfig.ServiceLogFilePath}";
}
return message;
}
}
}

View File

@@ -90,36 +90,29 @@
</ItemGroup>
<ItemGroup>
<Compile Include="App.cs" />
<Compile Include="Operations\ClientOperation.cs" />
<Compile Include="Operations\ClientTerminationOperation.cs" />
<Compile Include="Operations\ConfigurationBaseOperation.cs" />
<Compile Include="Operations\ConfigurationOperation.cs" />
<Compile Include="Operations\DisclaimerOperation.cs" />
<Compile Include="Operations\DisplayMonitorOperation.cs" />
<Compile Include="Operations\Events\ClientConfigurationErrorMessageArgs.cs" />
<Compile Include="Operations\Events\ConfigurationCompletedEventArgs.cs" />
<Compile Include="Operations\Events\ExamSelectionEventArgs.cs" />
<Compile Include="Operations\Events\InvalidDataMessageArgs.cs" />
<Compile Include="Operations\Events\InvalidPasswordMessageArgs.cs" />
<Compile Include="Operations\Events\MessageEventArgs.cs" />
<Compile Include="Operations\Events\NotSupportedMessageArgs.cs" />
<Compile Include="Operations\Events\PasswordRequiredEventArgs.cs" />
<Compile Include="Operations\Events\ServerFailureEventArgs.cs" />
<Compile Include="Operations\Events\UnexpectedErrorMessageArgs.cs" />
<Compile Include="Operations\ApplicationIntegrityOperation.cs" />
<Compile Include="Operations\Events\VersionRestrictionMessageArgs.cs" />
<Compile Include="Operations\KioskModeOperation.cs" />
<Compile Include="Operations\RemoteSessionOperation.cs" />
<Compile Include="Operations\ServerOperation.cs" />
<Compile Include="Operations\ServiceOperation.cs" />
<Compile Include="Operations\SessionActivationOperation.cs" />
<Compile Include="Operations\SessionIntegrityOperation.cs" />
<Compile Include="Operations\SessionOperation.cs" />
<Compile Include="Operations\SessionInitializationOperation.cs" />
<Compile Include="Communication\ClientBridge.cs" />
<Compile Include="Operations\Bootstrap\BootstrapOperationSequence.cs" />
<Compile Include="Operations\Session\ClientOperation.cs" />
<Compile Include="Operations\Session\ClientTerminationOperation.cs" />
<Compile Include="Operations\Session\ConfigurationBaseOperation.cs" />
<Compile Include="Operations\Session\ConfigurationOperation.cs" />
<Compile Include="Operations\Session\DisclaimerOperation.cs" />
<Compile Include="Operations\Session\DisplayMonitorOperation.cs" />
<Compile Include="Operations\Bootstrap\ApplicationIntegrityOperation.cs" />
<Compile Include="Operations\Session\KioskModeOperation.cs" />
<Compile Include="Operations\Session\RemoteSessionOperation.cs" />
<Compile Include="Operations\Session\SessionOperationSequence.cs" />
<Compile Include="Operations\Session\ServerOperation.cs" />
<Compile Include="Operations\Session\ServiceOperation.cs" />
<Compile Include="Operations\Session\SessionActivationOperation.cs" />
<Compile Include="Operations\Session\SessionIntegrityOperation.cs" />
<Compile Include="Operations\Session\Dependencies.cs" />
<Compile Include="Operations\Session\SessionOperation.cs" />
<Compile Include="Operations\Session\SessionInitializationOperation.cs" />
<Compile Include="Communication\RuntimeHost.cs" />
<Compile Include="CompositionRoot.cs" />
<Compile Include="Operations\VersionRestrictionOperation.cs" />
<Compile Include="Operations\VirtualMachineOperation.cs" />
<Compile Include="Operations\Session\VersionRestrictionOperation.cs" />
<Compile Include="Operations\Session\VirtualMachineOperation.cs" />
<Compile Include="Properties\AssemblyInfo.cs">
<SubType>Code</SubType>
</Compile>
@@ -133,8 +126,15 @@
<DependentUpon>Settings.settings</DependentUpon>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
<Compile Include="Responsibilities\ClientResponsibility.cs" />
<Compile Include="Responsibilities\CommunicationResponsibility.cs" />
<Compile Include="Responsibilities\ErrorMessageResponsibility.cs" />
<Compile Include="Responsibilities\RuntimeResponsibility.cs" />
<Compile Include="Responsibilities\RuntimeTask.cs" />
<Compile Include="Responsibilities\ServiceResponsibility.cs" />
<Compile Include="Responsibilities\SessionResponsibility.cs" />
<Compile Include="RuntimeController.cs" />
<Compile Include="SessionContext.cs" />
<Compile Include="RuntimeContext.cs" />
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>