diff --git a/SafeExamBrowser.Client/ClientContext.cs b/SafeExamBrowser.Client/ClientContext.cs index 0984543c..96f2e2ea 100644 --- a/SafeExamBrowser.Client/ClientContext.cs +++ b/SafeExamBrowser.Client/ClientContext.cs @@ -13,6 +13,7 @@ using SafeExamBrowser.Browser.Contracts; using SafeExamBrowser.Communication.Contracts.Hosts; using SafeExamBrowser.Configuration.Contracts; using SafeExamBrowser.Configuration.Contracts.Integrity; +using SafeExamBrowser.Proctoring.Contracts; using SafeExamBrowser.Server.Contracts; using SafeExamBrowser.Settings; using SafeExamBrowser.UserInterface.Contracts.Shell; @@ -54,6 +55,11 @@ namespace SafeExamBrowser.Client /// internal IIntegrityModule IntegrityModule { get; set; } + /// + /// The proctoring controller to be used if the current session has proctoring enabled. + /// + internal IProctoringController Proctoring { get; set; } + /// /// The server proxy to be used if the current session mode is . /// diff --git a/SafeExamBrowser.Client/ClientController.cs b/SafeExamBrowser.Client/ClientController.cs index 209cd66b..5a325a03 100644 --- a/SafeExamBrowser.Client/ClientController.cs +++ b/SafeExamBrowser.Client/ClientController.cs @@ -30,6 +30,8 @@ using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.Monitoring.Contracts.Applications; using SafeExamBrowser.Monitoring.Contracts.Display; using SafeExamBrowser.Monitoring.Contracts.System; +using SafeExamBrowser.Proctoring.Contracts; +using SafeExamBrowser.Proctoring.Contracts.Events; using SafeExamBrowser.Server.Contracts; using SafeExamBrowser.Server.Contracts.Data; using SafeExamBrowser.Settings; @@ -68,6 +70,7 @@ namespace SafeExamBrowser.Client private IBrowserApplication Browser => context.Browser; private IClientHost ClientHost => context.ClientHost; private IIntegrityModule IntegrityModule => context.IntegrityModule; + private IProctoringController Proctoring => context.Proctoring; private IServerProxy Server => context.Server; private AppSettings Settings => context.Settings; @@ -230,16 +233,6 @@ namespace SafeExamBrowser.Client } } - private void Taskbar_LoseFocusRequested(bool forward) - { - Browser.Focus(forward); - } - - private void Browser_LoseFocusRequested(bool forward) - { - taskbar.Focus(forward); - } - private void DeregisterEvents() { actionCenter.QuitButtonClicked -= Shell_QuitButtonClicked; @@ -249,6 +242,7 @@ namespace SafeExamBrowser.Client registry.ValueChanged -= Registry_ValueChanged; runtime.ConnectionLost -= Runtime_ConnectionLost; systemMonitor.SessionChanged -= SystemMonitor_SessionChanged; + taskbar.LoseFocusRequested -= Taskbar_LoseFocusRequested; taskbar.QuitButtonClicked -= Shell_QuitButtonClicked; if (Browser != null) @@ -327,6 +321,29 @@ namespace SafeExamBrowser.Client } } + private void PrepareShutdown() + { + FinalizeProctoring(); + } + + private void FinalizeProctoring() + { + if (Proctoring != default && Proctoring.HasRemainingWork()) + { + var dialog = uiFactory.CreateProctoringFinalizationDialog(); + var handler = new RemainingWorkUpdatedEventHandler((args) => dialog.Update(args)); + + Task.Run(() => + { + Proctoring.RemainingWorkUpdated += handler; + Proctoring.ExecuteRemainingWork(); + Proctoring.RemainingWorkUpdated -= handler; + }); + + dialog.Show(); + } + } + private void ScheduleIntegrityVerification() { const int FIVE_MINUTES = 300000; @@ -510,6 +527,8 @@ namespace SafeExamBrowser.Client { if (success) { + PrepareShutdown(); + var communication = runtime.RequestReconfiguration(filePath, url); if (communication.Success) @@ -531,6 +550,11 @@ namespace SafeExamBrowser.Client } } + private void Browser_LoseFocusRequested(bool forward) + { + taskbar.Focus(forward); + } + private void Browser_UserIdentifierDetected(string identifier) { if (Settings.SessionMode == SessionMode.Server) @@ -876,6 +900,11 @@ namespace SafeExamBrowser.Client } } + private void Taskbar_LoseFocusRequested(bool forward) + { + Browser.Focus(forward); + } + private void TerminationActivator_Activated() { PauseActivators(); @@ -1023,19 +1052,19 @@ namespace SafeExamBrowser.Client private bool TryInitiateShutdown() { var hasQuitPassword = !string.IsNullOrEmpty(Settings.Security.QuitPasswordHash); - var requestShutdown = false; + var initiateShutdown = false; var succes = false; if (hasQuitPassword) { - requestShutdown = TryValidateQuitPassword(); + initiateShutdown = TryValidateQuitPassword(); } else { - requestShutdown = TryConfirmShutdown(); + initiateShutdown = TryConfirmShutdown(); } - if (requestShutdown) + if (initiateShutdown) { succes = TryRequestShutdown(); } @@ -1084,6 +1113,8 @@ namespace SafeExamBrowser.Client private bool TryRequestShutdown() { + PrepareShutdown(); + var communication = runtime.RequestShutdown(); if (!communication.Success) diff --git a/SafeExamBrowser.Client/CompositionRoot.cs b/SafeExamBrowser.Client/CompositionRoot.cs index 2797b2cb..34ede80c 100644 --- a/SafeExamBrowser.Client/CompositionRoot.cs +++ b/SafeExamBrowser.Client/CompositionRoot.cs @@ -301,6 +301,8 @@ namespace SafeExamBrowser.Client uiFactory); var operation = new ProctoringOperation(actionCenter, context, controller, logger, taskbar, uiFactory); + context.Proctoring = controller; + return operation; } diff --git a/SafeExamBrowser.I18n.Contracts/TextKey.cs b/SafeExamBrowser.I18n.Contracts/TextKey.cs index 2c4fbf76..ab02bcfb 100644 --- a/SafeExamBrowser.I18n.Contracts/TextKey.cs +++ b/SafeExamBrowser.I18n.Contracts/TextKey.cs @@ -232,6 +232,14 @@ namespace SafeExamBrowser.I18n.Contracts PasswordDialog_QuitPasswordRequiredTitle, PasswordDialog_SettingsPasswordRequired, PasswordDialog_SettingsPasswordRequiredTitle, + // TODO: Translate text for ProctoringFinalizationDialog_... to remaining languages! + ProctoringFinalizationDialog_Confirm, + ProctoringFinalizationDialog_FailureMessage, + ProctoringFinalizationDialog_InfoMessage, + ProctoringFinalizationDialog_Status, + ProctoringFinalizationDialog_StatusAndTime, + ProctoringFinalizationDialog_StatusWaiting, + ProctoringFinalizationDialog_Title, RuntimeWindow_ApplicationRunning, ServerFailureDialog_Abort, ServerFailureDialog_Fallback, diff --git a/SafeExamBrowser.I18n/Data/de.xml b/SafeExamBrowser.I18n/Data/de.xml index 3f2deb78..9d93f252 100644 --- a/SafeExamBrowser.I18n/Data/de.xml +++ b/SafeExamBrowser.I18n/Data/de.xml @@ -654,6 +654,27 @@ Passwort erforderlich + + Bestätigen + + + Die verbleibenden Operationen konnten nicht abgeschlossen werden, da ein Problem mit dem Netzwerk und/oder dem Bildschirmüberwachungs-Dienst vorliegt. Die zwischengespeicherten Daten befinden sich im folgenden Verzeichnis: + + + Bitte warten Sie, während die Bildschirmüberwachung ihre restlichen Operationen ausführt. Dies kann eine Weile dauern, je nach Status des Netzwerkes und Bildschirmüberwachungs-Dienstes. + + + Ausführen der Übertragungsoperation %%_COUNT_%% von %%_TOTAL_%%. + + + Warten auf die Ausführung der Übertragungsoperation %%_COUNT_%% von %%_TOTAL_%% um %%_TIME_%%... + + + Warten auf die Wiederaufnahme von %%_COUNT_%% Übertragungsoperationen um %%_TIME_%%... + + + Finalisierung der Bildschirmüberwachung + SEB wird ausgeführt. diff --git a/SafeExamBrowser.I18n/Data/en.xml b/SafeExamBrowser.I18n/Data/en.xml index 2cabb2b6..b8f795dc 100644 --- a/SafeExamBrowser.I18n/Data/en.xml +++ b/SafeExamBrowser.I18n/Data/en.xml @@ -654,6 +654,27 @@ Password Required + + Confirm + + + The remaining operations could not be completed as there is a problem with the network and/or screen proctoring service. The cached data can be found in the following directory: + + + Please wait while the screen procotoring is executing its remaining operations. This may take a while, depending on the network and screen proctoring service status. + + + Executing transmission operation %%_COUNT_%% of %%_TOTAL_%%. + + + Waiting to execute transmission operation %%_COUNT_%% of %%_TOTAL_%% at %%_TIME_%%... + + + Waiting to resume %%_COUNT_%% transmission operations at %%_TIME_%%... + + + Screen Proctoring Finalization + SEB is running. diff --git a/SafeExamBrowser.Proctoring.Contracts/Events/RemainingWorkUpdatedEventArgs.cs b/SafeExamBrowser.Proctoring.Contracts/Events/RemainingWorkUpdatedEventArgs.cs new file mode 100644 index 00000000..1259cad9 --- /dev/null +++ b/SafeExamBrowser.Proctoring.Contracts/Events/RemainingWorkUpdatedEventArgs.cs @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +using System; + +namespace SafeExamBrowser.Proctoring.Contracts.Events +{ + /// + /// The event arguments used for the remaining work updated event fired by the . + /// + public class RemainingWorkUpdatedEventArgs + { + /// + /// The path of the local cache, if is true. + /// + public string CachePath { get; set; } + + /// + /// Indicates that the execution of the remaining work has failed. + /// + public bool HasFailed { get; set; } + + /// + /// Indicates that the execution of the remaining work has finished. + /// + public bool IsFinished { get; set; } + + /// + /// Indicates that the execution is paused resp. waiting to be resumed. + /// + public bool IsWaiting { get; set; } + + /// + /// The point in time when the next transmission will take place, if available. + /// + public DateTime? Next { get; set; } + + /// + /// The number of already executed work items. + /// + public int Progress { get; set; } + + /// + /// The point in time when the execution will resume, if is true. + /// + public DateTime Resume { get; set; } + + /// + /// The total number of work items to be executed. + /// + public int Total { get; set; } + } +} diff --git a/SafeExamBrowser.Proctoring.Contracts/Events/RemainingWorkUpdatedEventHandler.cs b/SafeExamBrowser.Proctoring.Contracts/Events/RemainingWorkUpdatedEventHandler.cs new file mode 100644 index 00000000..6e5463b0 --- /dev/null +++ b/SafeExamBrowser.Proctoring.Contracts/Events/RemainingWorkUpdatedEventHandler.cs @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2023 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +namespace SafeExamBrowser.Proctoring.Contracts.Events +{ + /// + /// Event handler used to indicate that the remaining work status has been updated. + /// + public delegate void RemainingWorkUpdatedEventHandler(RemainingWorkUpdatedEventArgs args); +} diff --git a/SafeExamBrowser.Proctoring.Contracts/IProctoringController.cs b/SafeExamBrowser.Proctoring.Contracts/IProctoringController.cs index 6fdd4f7c..fcc05ddb 100644 --- a/SafeExamBrowser.Proctoring.Contracts/IProctoringController.cs +++ b/SafeExamBrowser.Proctoring.Contracts/IProctoringController.cs @@ -38,6 +38,21 @@ namespace SafeExamBrowser.Proctoring.Contracts /// event ProctoringEventHandler HandRaised; + /// + /// Event fired when the status of the remaining work has been updated. + /// + event RemainingWorkUpdatedEventHandler RemainingWorkUpdated; + + /// + /// Executes any remaining work like e.g. the transmission of cached screen shots. Make sure to do so before calling . + /// + void ExecuteRemainingWork(); + + /// + /// Indicates whether there is any remaining work which needs to be done before the proctoring can be terminated. + /// + bool HasRemainingWork(); + /// /// Initializes the given settings and starts the proctoring if the settings are valid. /// @@ -54,7 +69,7 @@ namespace SafeExamBrowser.Proctoring.Contracts void RaiseHand(string message = default); /// - /// Stops the proctoring functionality. + /// Stops the proctoring functionality. Make sure to call beforehand if necessary. /// void Terminate(); } diff --git a/SafeExamBrowser.Proctoring.Contracts/SafeExamBrowser.Proctoring.Contracts.csproj b/SafeExamBrowser.Proctoring.Contracts/SafeExamBrowser.Proctoring.Contracts.csproj index 06715f3e..b33d0e30 100644 --- a/SafeExamBrowser.Proctoring.Contracts/SafeExamBrowser.Proctoring.Contracts.csproj +++ b/SafeExamBrowser.Proctoring.Contracts/SafeExamBrowser.Proctoring.Contracts.csproj @@ -56,6 +56,8 @@ + + diff --git a/SafeExamBrowser.Proctoring/JitsiMeet/JitsiMeetImplementation.cs b/SafeExamBrowser.Proctoring/JitsiMeet/JitsiMeetImplementation.cs index 4bc3f8f5..303a4ee8 100644 --- a/SafeExamBrowser.Proctoring/JitsiMeet/JitsiMeetImplementation.cs +++ b/SafeExamBrowser.Proctoring/JitsiMeet/JitsiMeetImplementation.cs @@ -13,7 +13,6 @@ using System.Threading; using System.Windows; using Microsoft.Web.WebView2.Wpf; using SafeExamBrowser.Configuration.Contracts; -using SafeExamBrowser.Core.Contracts.Notifications.Events; using SafeExamBrowser.Core.Contracts.Resources.Icons; using SafeExamBrowser.I18n.Contracts; using SafeExamBrowser.Logging.Contracts; @@ -41,8 +40,6 @@ namespace SafeExamBrowser.Proctoring.JitsiMeet internal override string Name => nameof(JitsiMeet); - public override event NotificationChangedEventHandler NotificationChanged; - internal JitsiMeetImplementation( AppConfig appConfig, IFileSystem fileSystem, @@ -194,7 +191,6 @@ namespace SafeExamBrowser.Proctoring.JitsiMeet internal override void Terminate() { Stop(); - TerminateNotification(); logger.Info("Terminated proctoring."); } @@ -210,11 +206,6 @@ namespace SafeExamBrowser.Proctoring.JitsiMeet } } - protected override void TerminateNotification() - { - // Nothing to do here for now. - } - private string LoadContent(ProctoringSettings settings) { var assembly = Assembly.GetAssembly(typeof(ProctoringController)); @@ -249,7 +240,7 @@ namespace SafeExamBrowser.Proctoring.JitsiMeet IconResource = new XamlIconResource { Uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/Proctoring_Active.xaml") }; Tooltip = text.Get(TextKey.Notification_ProctoringActiveTooltip); - NotificationChanged?.Invoke(); + InvokeNotificationChanged(); } private void ShowNotificationInactive() @@ -258,7 +249,7 @@ namespace SafeExamBrowser.Proctoring.JitsiMeet IconResource = new XamlIconResource { Uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/Proctoring_Inactive.xaml") }; Tooltip = text.Get(TextKey.Notification_ProctoringInactiveTooltip); - NotificationChanged?.Invoke(); + InvokeNotificationChanged(); } } } diff --git a/SafeExamBrowser.Proctoring/ProctoringController.cs b/SafeExamBrowser.Proctoring/ProctoringController.cs index 7b45b66d..3691f5a5 100644 --- a/SafeExamBrowser.Proctoring/ProctoringController.cs +++ b/SafeExamBrowser.Proctoring/ProctoringController.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; +using KGySoft.CoreLibraries; using SafeExamBrowser.Browser.Contracts; using SafeExamBrowser.Configuration.Contracts; using SafeExamBrowser.Core.Contracts.Notifications; @@ -38,6 +39,11 @@ namespace SafeExamBrowser.Proctoring public event ProctoringEventHandler HandLowered; public event ProctoringEventHandler HandRaised; + public event RemainingWorkUpdatedEventHandler RemainingWorkUpdated + { + add { implementations.ForEach(i => i.RemainingWorkUpdated += value); } + remove { implementations.ForEach(i => i.RemainingWorkUpdated -= value); } + } public ProctoringController( AppConfig appConfig, @@ -57,6 +63,43 @@ namespace SafeExamBrowser.Proctoring implementations = new List(); } + public void ExecuteRemainingWork() + { + foreach (var implementation in implementations) + { + try + { + implementation.ExecuteRemainingWork(); + } + catch (Exception e) + { + logger.Error($"Failed to execute remaining work for '{implementation.Name}'!", e); + } + } + } + + public bool HasRemainingWork() + { + var hasWork = false; + + foreach (var implementation in implementations) + { + try + { + if (implementation.HasRemainingWork()) + { + hasWork = true; + } + } + catch (Exception e) + { + logger.Error($"Failed to check whether has remaining work for '{implementation.Name}'!", e); + } + } + + return hasWork; + } + public void Initialize(ProctoringSettings settings) { implementations = factory.CreateAllActive(settings); diff --git a/SafeExamBrowser.Proctoring/ProctoringImplementation.cs b/SafeExamBrowser.Proctoring/ProctoringImplementation.cs index 48e1e7eb..0dc67e0c 100644 --- a/SafeExamBrowser.Proctoring/ProctoringImplementation.cs +++ b/SafeExamBrowser.Proctoring/ProctoringImplementation.cs @@ -9,6 +9,7 @@ using SafeExamBrowser.Core.Contracts.Notifications; using SafeExamBrowser.Core.Contracts.Notifications.Events; using SafeExamBrowser.Core.Contracts.Resources.Icons; +using SafeExamBrowser.Proctoring.Contracts.Events; using SafeExamBrowser.Server.Contracts.Events.Proctoring; namespace SafeExamBrowser.Proctoring @@ -21,7 +22,8 @@ namespace SafeExamBrowser.Proctoring public string Tooltip { get; protected set; } public IconResource IconResource { get; protected set; } - public abstract event NotificationChangedEventHandler NotificationChanged; + internal event RemainingWorkUpdatedEventHandler RemainingWorkUpdated; + public event NotificationChangedEventHandler NotificationChanged; void INotification.Activate() { @@ -40,7 +42,20 @@ namespace SafeExamBrowser.Proctoring internal abstract void Stop(); internal abstract void Terminate(); + internal virtual void ExecuteRemainingWork() { } + internal virtual bool HasRemainingWork() => false; + protected virtual void ActivateNotification() { } protected virtual void TerminateNotification() { } + + protected void InvokeNotificationChanged() + { + NotificationChanged?.Invoke(); + } + + protected void InvokeRemainingWorkUpdated(RemainingWorkUpdatedEventArgs args) + { + RemainingWorkUpdated?.Invoke(args); + } } } diff --git a/SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj b/SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj index 84f4b465..178e3fe6 100644 --- a/SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj +++ b/SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj @@ -93,6 +93,7 @@ + diff --git a/SafeExamBrowser.Proctoring/ScreenProctoring/Buffer.cs b/SafeExamBrowser.Proctoring/ScreenProctoring/Buffer.cs new file mode 100644 index 00000000..a8917e86 --- /dev/null +++ b/SafeExamBrowser.Proctoring/ScreenProctoring/Buffer.cs @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2023 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using SafeExamBrowser.Logging.Contracts; +using SafeExamBrowser.Proctoring.ScreenProctoring.Data; +using SafeExamBrowser.Proctoring.ScreenProctoring.Imaging; + +namespace SafeExamBrowser.Proctoring.ScreenProctoring +{ + internal class Buffer + { + private readonly object @lock = new object(); + + private readonly List<(MetaData metaData, DateTime schedule, ScreenShot screenShot)> list; + private readonly ILogger logger; + + internal int Count + { + get + { + lock (@lock) + { + return list.Count; + } + } + } + + internal Buffer(ILogger logger) + { + this.list = new List<(MetaData, DateTime, ScreenShot)>(); + this.logger = logger; + } + + internal bool Any() + { + lock (@lock) + { + return list.Any(); + } + } + + internal void Dequeue() + { + lock (@lock) + { + if (list.Any()) + { + var (_, schedule, screenShot) = list.First(); + + list.RemoveAt(0); + logger.Debug($"Removed data for '{screenShot.CaptureTime:HH:mm:ss} -> {schedule:HH:mm:ss}', {Count} item(s) remaining."); + } + } + } + + internal void Enqueue(MetaData metaData, DateTime schedule, ScreenShot screenShot) + { + lock (@lock) + { + list.Add((metaData, schedule, screenShot)); + list.Sort((a, b) => DateTime.Compare(a.schedule, b.schedule)); + + logger.Debug($"Buffered data for '{screenShot.CaptureTime:HH:mm:ss} -> {schedule:HH:mm:ss}', now holding {Count} item(s)."); + } + } + + internal bool TryPeek(out MetaData metaData, out DateTime schedule, out ScreenShot screenShot) + { + lock (@lock) + { + metaData = default; + schedule = default; + screenShot = default; + + if (list.Any()) + { + (metaData, schedule, screenShot) = list.First(); + } + + return metaData != default && screenShot != default; + } + } + } +} diff --git a/SafeExamBrowser.Proctoring/ScreenProctoring/Cache.cs b/SafeExamBrowser.Proctoring/ScreenProctoring/Cache.cs index 70f2b316..32b5e2b2 100644 --- a/SafeExamBrowser.Proctoring/ScreenProctoring/Cache.cs +++ b/SafeExamBrowser.Proctoring/ScreenProctoring/Cache.cs @@ -7,7 +7,7 @@ */ using System; -using System.Collections.Generic; +using System.Collections.Concurrent; using System.IO; using System.Linq; using System.Security.Cryptography; @@ -27,20 +27,21 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring private readonly AppConfig appConfig; private readonly ILogger logger; - private readonly Queue<(string fileName, int checksum, string hash)> queue; + private readonly ConcurrentQueue<(string fileName, int checksum, string hash)> queue; - private string Directory { get; set; } + internal int Count => queue.Count; + internal string Directory { get; private set; } public Cache(AppConfig appConfig, ILogger logger) { this.appConfig = appConfig; this.logger = logger; - this.queue = new Queue<(string, int, string)>(); + this.queue = new ConcurrentQueue<(string, int, string)>(); } internal bool Any() { - return false; + return queue.Any(); } internal bool TryEnqueue(MetaData metaData, ScreenShot screenShot) @@ -57,7 +58,7 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring success = true; - logger.Debug($"Cached data for '{fileName}'."); + logger.Debug($"Cached data for '{fileName}', now holding {Count} item(s)."); } catch (Exception e) { @@ -74,23 +75,21 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring metaData = default; screenShot = default; - if (queue.Any()) + if (queue.Any() && queue.TryPeek(out var item)) { - var (fileName, checksum, hash) = queue.Peek(); - try { - LoadData(fileName, out metaData, out screenShot); - LoadImage(fileName, screenShot); - Dequeue(fileName, checksum, hash, metaData, screenShot); + LoadData(item.fileName, out metaData, out screenShot); + LoadImage(item.fileName, screenShot); + Dequeue(item.fileName, item.checksum, item.hash, metaData, screenShot); success = true; - logger.Debug($"Uncached data for '{fileName}'."); + logger.Debug($"Removed data for '{item.fileName}', {Count} item(s) remaining."); } catch (Exception e) { - logger.Error($"Failed to uncache data for '{fileName}'!", e); + logger.Error($"Failed to remove data for '{item.fileName}'!", e); } } @@ -116,7 +115,7 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring File.Delete(dataPath); File.Delete(imagePath); - queue.Dequeue(); + while (!queue.TryDequeue(out _)) ; } private void Enqueue(string fileName, MetaData metaData, ScreenShot screenShot) diff --git a/SafeExamBrowser.Proctoring/ScreenProctoring/ScreenProctoringImplementation.cs b/SafeExamBrowser.Proctoring/ScreenProctoring/ScreenProctoringImplementation.cs index f09e86a1..0a5b7740 100644 --- a/SafeExamBrowser.Proctoring/ScreenProctoring/ScreenProctoringImplementation.cs +++ b/SafeExamBrowser.Proctoring/ScreenProctoring/ScreenProctoringImplementation.cs @@ -9,7 +9,6 @@ using System; using SafeExamBrowser.Browser.Contracts; using SafeExamBrowser.Configuration.Contracts; -using SafeExamBrowser.Core.Contracts.Notifications.Events; using SafeExamBrowser.Core.Contracts.Resources.Icons; using SafeExamBrowser.I18n.Contracts; using SafeExamBrowser.Logging.Contracts; @@ -34,8 +33,6 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring internal override string Name => nameof(ScreenProctoring); - public override event NotificationChangedEventHandler NotificationChanged; - internal ScreenProctoringImplementation( AppConfig appConfig, IApplicationMonitor applicationMonitor, @@ -54,6 +51,29 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring this.text = text; } + internal override void ExecuteRemainingWork() + { + logger.Info("Starting execution of remaining work..."); + spooler.ExecuteRemainingWork(InvokeRemainingWorkUpdated); + logger.Info("Terminated execution of remaining work."); + } + + internal override bool HasRemainingWork() + { + var hasWork = spooler.HasRemainingWork(); + + if (hasWork) + { + logger.Info("There is remaining work to be done."); + } + else + { + logger.Info("There is no remaining work to be done."); + } + + return hasWork; + } + internal override void Initialize() { var start = true; @@ -132,10 +152,7 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring internal override void Terminate() { - // TODO: Cache transmission or user information! - Stop(); - TerminateNotification(); logger.Info("Terminated proctoring."); } @@ -189,7 +206,7 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring Tooltip = text.Get(TextKey.Notification_ProctoringInactiveTooltip); } - NotificationChanged?.Invoke(); + InvokeNotificationChanged(); } } } diff --git a/SafeExamBrowser.Proctoring/ScreenProctoring/TransmissionSpooler.cs b/SafeExamBrowser.Proctoring/ScreenProctoring/TransmissionSpooler.cs index 855b5654..e6c58944 100644 --- a/SafeExamBrowser.Proctoring/ScreenProctoring/TransmissionSpooler.cs +++ b/SafeExamBrowser.Proctoring/ScreenProctoring/TransmissionSpooler.cs @@ -8,12 +8,11 @@ using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Timers; using SafeExamBrowser.Configuration.Contracts; using SafeExamBrowser.Logging.Contracts; +using SafeExamBrowser.Proctoring.Contracts.Events; using SafeExamBrowser.Proctoring.ScreenProctoring.Data; using SafeExamBrowser.Proctoring.ScreenProctoring.Imaging; using SafeExamBrowser.Proctoring.ScreenProctoring.Service; @@ -23,9 +22,10 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring { internal class TransmissionSpooler { - const int BAD = 10; - const int GOOD = 0; + private const int BAD = 10; + private const int GOOD = 0; + private readonly Buffer buffer; private readonly Cache cache; private readonly ILogger logger; private readonly ConcurrentQueue<(MetaData metaData, ScreenShot screenShot)> queue; @@ -33,8 +33,8 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring private readonly ServiceProxy service; private readonly Timer timer; - private Queue<(MetaData metaData, DateTime schedule, ScreenShot screenShot)> buffer; private int health; + private bool networkIssue; private bool recovering; private DateTime resume; private Thread thread; @@ -42,7 +42,7 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring internal TransmissionSpooler(AppConfig appConfig, IModuleLogger logger, ServiceProxy service) { - this.buffer = new Queue<(MetaData, DateTime, ScreenShot)>(); + this.buffer = new Buffer(logger.CloneFor(nameof(Buffer))); this.cache = new Cache(appConfig, logger.CloneFor(nameof(Cache))); this.logger = logger; this.queue = new ConcurrentQueue<(MetaData, ScreenShot)>(); @@ -56,6 +56,55 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring queue.Enqueue((metaData, screenShot)); } + internal void ExecuteRemainingWork(Action updateStatus) + { + var previous = buffer.Count + cache.Count; + var progress = 0; + var total = previous; + + while (HasRemainingWork() && service.IsConnected && (!networkIssue || recovering)) + { + var remaining = buffer.Count + cache.Count; + + if (total < remaining) + { + total = remaining; + } + else if (previous < remaining) + { + total += remaining - previous; + } + + previous = remaining; + progress = total - remaining; + + updateStatus(new RemainingWorkUpdatedEventArgs + { + IsWaiting = recovering, + Next = buffer.TryPeek(out _, out var schedule, out _) ? schedule : default(DateTime?), + Progress = progress, + Resume = resume, + Total = total + }); + + Thread.Sleep(100); + } + + if (networkIssue) + { + updateStatus(new RemainingWorkUpdatedEventArgs { HasFailed = true, CachePath = cache.Directory }); + } + else + { + updateStatus(new RemainingWorkUpdatedEventArgs { IsFinished = true }); + } + } + + internal bool HasRemainingWork() + { + return buffer.Any() || cache.Any(); + } + internal void Start() { const int FIFTEEN_SECONDS = 15000; @@ -167,14 +216,14 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring { BufferFromCache(); BufferFromQueue(); - TryTransmitFromBuffer(); + TransmitFromBuffer(); } private void ExecuteNormally() { - TryTransmitFromBuffer(); - TryTransmitFromCache(); - TryTransmitFromQueue(); + TransmitFromBuffer(); + TransmitFromCache(); + TransmitFromQueue(); } private void ExecuteRecovery() @@ -194,17 +243,11 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring } } - private void Buffer(MetaData metaData, DateTime schedule, ScreenShot screenShot) - { - buffer.Enqueue((metaData, schedule, screenShot)); - buffer = new Queue<(MetaData, DateTime, ScreenShot)>(buffer.OrderBy((b) => b.schedule)); - } - private void BufferFromCache() { if (cache.TryDequeue(out var metaData, out var screenShot)) { - Buffer(metaData, CalculateSchedule(metaData), screenShot); + buffer.Enqueue(metaData, CalculateSchedule(metaData), screenShot); } } @@ -212,21 +255,16 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring { if (TryDequeue(out var metaData, out var screenShot)) { - Buffer(metaData, CalculateSchedule(metaData), screenShot); + buffer.Enqueue(metaData, CalculateSchedule(metaData), screenShot); } } private void CacheFromBuffer() { - if (TryPeekFromBuffer(out var metaData, out _, out var screenShot)) + if (buffer.TryPeek(out var metaData, out _, out var screenShot) && cache.TryEnqueue(metaData, screenShot)) { - var success = cache.TryEnqueue(metaData, screenShot); - - if (success) - { - buffer.Dequeue(); - screenShot.Dispose(); - } + buffer.Dequeue(); + screenShot.Dispose(); } } @@ -234,15 +272,13 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring { if (TryDequeue(out var metaData, out var screenShot)) { - var success = cache.TryEnqueue(metaData, screenShot); - - if (success) + if (cache.TryEnqueue(metaData, screenShot)) { screenShot.Dispose(); } else { - Buffer(metaData, DateTime.Now, screenShot); + buffer.Enqueue(metaData, DateTime.Now, screenShot); } } } @@ -255,6 +291,48 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring return schedule; } + private void TransmitFromBuffer() + { + var hasItem = buffer.TryPeek(out var metaData, out var schedule, out var screenShot); + var ready = schedule <= DateTime.Now; + + if (hasItem && ready && TryTransmit(metaData, screenShot)) + { + buffer.Dequeue(); + screenShot.Dispose(); + } + } + + private void TransmitFromCache() + { + if (cache.TryDequeue(out var metaData, out var screenShot)) + { + if (TryTransmit(metaData, screenShot)) + { + screenShot.Dispose(); + } + else + { + buffer.Enqueue(metaData, DateTime.Now, screenShot); + } + } + } + + private void TransmitFromQueue() + { + if (TryDequeue(out var metaData, out var screenShot)) + { + if (TryTransmit(metaData, screenShot)) + { + screenShot.Dispose(); + } + else + { + buffer.Enqueue(metaData, DateTime.Now, screenShot); + } + } + } + private bool TryDequeue(out MetaData metaData, out ScreenShot screenShot) { metaData = default; @@ -269,80 +347,6 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring return metaData != default && screenShot != default; } - private bool TryPeekFromBuffer(out MetaData metaData, out DateTime schedule, out ScreenShot screenShot) - { - metaData = default; - schedule = default; - screenShot = default; - - if (buffer.Any()) - { - (metaData, schedule, screenShot) = buffer.Peek(); - } - - return metaData != default && screenShot != default; - } - - private bool TryTransmitFromBuffer() - { - var success = false; - - if (TryPeekFromBuffer(out var metaData, out var schedule, out var screenShot) && schedule <= DateTime.Now) - { - success = TryTransmit(metaData, screenShot); - - if (success) - { - buffer.Dequeue(); - screenShot.Dispose(); - } - } - - return success; - } - - private bool TryTransmitFromCache() - { - var success = true; - - if (cache.TryDequeue(out var metaData, out var screenShot)) - { - success = TryTransmit(metaData, screenShot); - - if (success) - { - screenShot.Dispose(); - } - else - { - Buffer(metaData, DateTime.Now, screenShot); - } - } - - return success; - } - - private bool TryTransmitFromQueue() - { - var success = false; - - if (TryDequeue(out var metaData, out var screenShot)) - { - success = TryTransmit(metaData, screenShot); - - if (success) - { - screenShot.Dispose(); - } - else - { - Buffer(metaData, DateTime.Now, screenShot); - } - } - - return success; - } - private bool TryTransmit(MetaData metaData, ScreenShot screenShot) { var success = false; @@ -351,10 +355,12 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring { var response = service.Send(metaData, screenShot); + networkIssue = !response.Success; + success = response.Success; + if (response.Success) { health = UpdateHealth(response.Value); - success = true; } } else @@ -371,6 +377,8 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring { var response = service.GetHealth(); + networkIssue = !response.Success; + if (response.Success) { health = UpdateHealth(response.Value); diff --git a/SafeExamBrowser.UserInterface.Contracts/IUserInterfaceFactory.cs b/SafeExamBrowser.UserInterface.Contracts/IUserInterfaceFactory.cs index 8d41b793..79398ce7 100644 --- a/SafeExamBrowser.UserInterface.Contracts/IUserInterfaceFactory.cs +++ b/SafeExamBrowser.UserInterface.Contracts/IUserInterfaceFactory.cs @@ -103,6 +103,11 @@ namespace SafeExamBrowser.UserInterface.Contracts /// ISystemControl CreatePowerSupplyControl(IPowerSupply powerSupply, Location location); + /// + /// Creates a new dialog to display the status of the proctoring finalization. + /// + IProctoringFinalizationDialog CreateProctoringFinalizationDialog(); + /// /// Creates a new proctoring window loaded with the given proctoring control. /// diff --git a/SafeExamBrowser.UserInterface.Contracts/Proctoring/IProctoringFinalizationDialog.cs b/SafeExamBrowser.UserInterface.Contracts/Proctoring/IProctoringFinalizationDialog.cs new file mode 100644 index 00000000..055198f3 --- /dev/null +++ b/SafeExamBrowser.UserInterface.Contracts/Proctoring/IProctoringFinalizationDialog.cs @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +using SafeExamBrowser.Proctoring.Contracts.Events; + +namespace SafeExamBrowser.UserInterface.Contracts.Proctoring +{ + /// + /// The dialog to display the status of the proctoring finalization. + /// + public interface IProctoringFinalizationDialog + { + /// + /// Shows the dialog as topmost window. + /// + void Show(); + + /// + /// Updates the status of the finalization. + /// + void Update(RemainingWorkUpdatedEventArgs status); + } +} diff --git a/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj b/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj index 726d8327..b75ee7b2 100644 --- a/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj +++ b/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj @@ -103,6 +103,7 @@ + diff --git a/SafeExamBrowser.UserInterface.Desktop/SafeExamBrowser.UserInterface.Desktop.csproj b/SafeExamBrowser.UserInterface.Desktop/SafeExamBrowser.UserInterface.Desktop.csproj index 558f3db6..9667b599 100644 --- a/SafeExamBrowser.UserInterface.Desktop/SafeExamBrowser.UserInterface.Desktop.csproj +++ b/SafeExamBrowser.UserInterface.Desktop/SafeExamBrowser.UserInterface.Desktop.csproj @@ -171,6 +171,9 @@ PasswordDialog.xaml + + ProctoringFinalizationDialog.xaml + ProctoringWindow.xaml @@ -391,6 +394,10 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + Designer MSBuild:Compile diff --git a/SafeExamBrowser.UserInterface.Desktop/UserInterfaceFactory.cs b/SafeExamBrowser.UserInterface.Desktop/UserInterfaceFactory.cs index 5b0344a4..a8322e45 100644 --- a/SafeExamBrowser.UserInterface.Desktop/UserInterfaceFactory.cs +++ b/SafeExamBrowser.UserInterface.Desktop/UserInterfaceFactory.cs @@ -177,6 +177,11 @@ namespace SafeExamBrowser.UserInterface.Desktop } } + public IProctoringFinalizationDialog CreateProctoringFinalizationDialog() + { + return Application.Current.Dispatcher.Invoke(() => new ProctoringFinalizationDialog(text)); + } + public IProctoringWindow CreateProctoringWindow(IProctoringControl control) { return Application.Current.Dispatcher.Invoke(() => new ProctoringWindow(control)); diff --git a/SafeExamBrowser.UserInterface.Desktop/Windows/ProctoringFinalizationDialog.xaml b/SafeExamBrowser.UserInterface.Desktop/Windows/ProctoringFinalizationDialog.xaml new file mode 100644 index 00000000..3f75ddc0 --- /dev/null +++ b/SafeExamBrowser.UserInterface.Desktop/Windows/ProctoringFinalizationDialog.xaml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +