SEBSP-107: Implemented screen proctoring finalization.

This commit is contained in:
Damian Büchel 2024-02-29 21:05:43 +01:00
parent 787c84cc0e
commit ff5b91c010
30 changed files with 927 additions and 155 deletions

View file

@ -13,6 +13,7 @@ using SafeExamBrowser.Browser.Contracts;
using SafeExamBrowser.Communication.Contracts.Hosts; using SafeExamBrowser.Communication.Contracts.Hosts;
using SafeExamBrowser.Configuration.Contracts; using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Configuration.Contracts.Integrity; using SafeExamBrowser.Configuration.Contracts.Integrity;
using SafeExamBrowser.Proctoring.Contracts;
using SafeExamBrowser.Server.Contracts; using SafeExamBrowser.Server.Contracts;
using SafeExamBrowser.Settings; using SafeExamBrowser.Settings;
using SafeExamBrowser.UserInterface.Contracts.Shell; using SafeExamBrowser.UserInterface.Contracts.Shell;
@ -54,6 +55,11 @@ namespace SafeExamBrowser.Client
/// </summary> /// </summary>
internal IIntegrityModule IntegrityModule { get; set; } internal IIntegrityModule IntegrityModule { get; set; }
/// <summary>
/// The proctoring controller to be used if the current session has proctoring enabled.
/// </summary>
internal IProctoringController Proctoring { get; set; }
/// <summary> /// <summary>
/// The server proxy to be used if the current session mode is <see cref="SessionMode.Server"/>. /// The server proxy to be used if the current session mode is <see cref="SessionMode.Server"/>.
/// </summary> /// </summary>

View file

@ -30,6 +30,8 @@ using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Monitoring.Contracts.Applications; using SafeExamBrowser.Monitoring.Contracts.Applications;
using SafeExamBrowser.Monitoring.Contracts.Display; using SafeExamBrowser.Monitoring.Contracts.Display;
using SafeExamBrowser.Monitoring.Contracts.System; using SafeExamBrowser.Monitoring.Contracts.System;
using SafeExamBrowser.Proctoring.Contracts;
using SafeExamBrowser.Proctoring.Contracts.Events;
using SafeExamBrowser.Server.Contracts; using SafeExamBrowser.Server.Contracts;
using SafeExamBrowser.Server.Contracts.Data; using SafeExamBrowser.Server.Contracts.Data;
using SafeExamBrowser.Settings; using SafeExamBrowser.Settings;
@ -68,6 +70,7 @@ namespace SafeExamBrowser.Client
private IBrowserApplication Browser => context.Browser; private IBrowserApplication Browser => context.Browser;
private IClientHost ClientHost => context.ClientHost; private IClientHost ClientHost => context.ClientHost;
private IIntegrityModule IntegrityModule => context.IntegrityModule; private IIntegrityModule IntegrityModule => context.IntegrityModule;
private IProctoringController Proctoring => context.Proctoring;
private IServerProxy Server => context.Server; private IServerProxy Server => context.Server;
private AppSettings Settings => context.Settings; 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() private void DeregisterEvents()
{ {
actionCenter.QuitButtonClicked -= Shell_QuitButtonClicked; actionCenter.QuitButtonClicked -= Shell_QuitButtonClicked;
@ -249,6 +242,7 @@ namespace SafeExamBrowser.Client
registry.ValueChanged -= Registry_ValueChanged; registry.ValueChanged -= Registry_ValueChanged;
runtime.ConnectionLost -= Runtime_ConnectionLost; runtime.ConnectionLost -= Runtime_ConnectionLost;
systemMonitor.SessionChanged -= SystemMonitor_SessionChanged; systemMonitor.SessionChanged -= SystemMonitor_SessionChanged;
taskbar.LoseFocusRequested -= Taskbar_LoseFocusRequested;
taskbar.QuitButtonClicked -= Shell_QuitButtonClicked; taskbar.QuitButtonClicked -= Shell_QuitButtonClicked;
if (Browser != null) 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() private void ScheduleIntegrityVerification()
{ {
const int FIVE_MINUTES = 300000; const int FIVE_MINUTES = 300000;
@ -510,6 +527,8 @@ namespace SafeExamBrowser.Client
{ {
if (success) if (success)
{ {
PrepareShutdown();
var communication = runtime.RequestReconfiguration(filePath, url); var communication = runtime.RequestReconfiguration(filePath, url);
if (communication.Success) 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) private void Browser_UserIdentifierDetected(string identifier)
{ {
if (Settings.SessionMode == SessionMode.Server) 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() private void TerminationActivator_Activated()
{ {
PauseActivators(); PauseActivators();
@ -1023,19 +1052,19 @@ namespace SafeExamBrowser.Client
private bool TryInitiateShutdown() private bool TryInitiateShutdown()
{ {
var hasQuitPassword = !string.IsNullOrEmpty(Settings.Security.QuitPasswordHash); var hasQuitPassword = !string.IsNullOrEmpty(Settings.Security.QuitPasswordHash);
var requestShutdown = false; var initiateShutdown = false;
var succes = false; var succes = false;
if (hasQuitPassword) if (hasQuitPassword)
{ {
requestShutdown = TryValidateQuitPassword(); initiateShutdown = TryValidateQuitPassword();
} }
else else
{ {
requestShutdown = TryConfirmShutdown(); initiateShutdown = TryConfirmShutdown();
} }
if (requestShutdown) if (initiateShutdown)
{ {
succes = TryRequestShutdown(); succes = TryRequestShutdown();
} }
@ -1084,6 +1113,8 @@ namespace SafeExamBrowser.Client
private bool TryRequestShutdown() private bool TryRequestShutdown()
{ {
PrepareShutdown();
var communication = runtime.RequestShutdown(); var communication = runtime.RequestShutdown();
if (!communication.Success) if (!communication.Success)

View file

@ -301,6 +301,8 @@ namespace SafeExamBrowser.Client
uiFactory); uiFactory);
var operation = new ProctoringOperation(actionCenter, context, controller, logger, taskbar, uiFactory); var operation = new ProctoringOperation(actionCenter, context, controller, logger, taskbar, uiFactory);
context.Proctoring = controller;
return operation; return operation;
} }

View file

@ -232,6 +232,14 @@ namespace SafeExamBrowser.I18n.Contracts
PasswordDialog_QuitPasswordRequiredTitle, PasswordDialog_QuitPasswordRequiredTitle,
PasswordDialog_SettingsPasswordRequired, PasswordDialog_SettingsPasswordRequired,
PasswordDialog_SettingsPasswordRequiredTitle, 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, RuntimeWindow_ApplicationRunning,
ServerFailureDialog_Abort, ServerFailureDialog_Abort,
ServerFailureDialog_Fallback, ServerFailureDialog_Fallback,

View file

@ -654,6 +654,27 @@
<Entry key="PasswordDialog_SettingsPasswordRequiredTitle"> <Entry key="PasswordDialog_SettingsPasswordRequiredTitle">
Passwort erforderlich Passwort erforderlich
</Entry> </Entry>
<Entry key="ProctoringFinalizationDialog_Confirm">
Bestätigen
</Entry>
<Entry key="ProctoringFinalizationDialog_FailureMessage">
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:
</Entry>
<Entry key="ProctoringFinalizationDialog_InfoMessage">
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.
</Entry>
<Entry key="ProctoringFinalizationDialog_Status">
Ausführen der Übertragungsoperation %%_COUNT_%% von %%_TOTAL_%%.
</Entry>
<Entry key="ProctoringFinalizationDialog_StatusAndTime">
Warten auf die Ausführung der Übertragungsoperation %%_COUNT_%% von %%_TOTAL_%% um %%_TIME_%%...
</Entry>
<Entry key="ProctoringFinalizationDialog_StatusWaiting">
Warten auf die Wiederaufnahme von %%_COUNT_%% Übertragungsoperationen um %%_TIME_%%...
</Entry>
<Entry key="ProctoringFinalizationDialog_Title">
Finalisierung der Bildschirmüberwachung
</Entry>
<Entry key="RuntimeWindow_ApplicationRunning"> <Entry key="RuntimeWindow_ApplicationRunning">
SEB wird ausgeführt. SEB wird ausgeführt.
</Entry> </Entry>

View file

@ -654,6 +654,27 @@
<Entry key="PasswordDialog_SettingsPasswordRequiredTitle"> <Entry key="PasswordDialog_SettingsPasswordRequiredTitle">
Password Required Password Required
</Entry> </Entry>
<Entry key="ProctoringFinalizationDialog_Confirm">
Confirm
</Entry>
<Entry key="ProctoringFinalizationDialog_FailureMessage">
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:
</Entry>
<Entry key="ProctoringFinalizationDialog_InfoMessage">
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.
</Entry>
<Entry key="ProctoringFinalizationDialog_Status">
Executing transmission operation %%_COUNT_%% of %%_TOTAL_%%.
</Entry>
<Entry key="ProctoringFinalizationDialog_StatusAndTime">
Waiting to execute transmission operation %%_COUNT_%% of %%_TOTAL_%% at %%_TIME_%%...
</Entry>
<Entry key="ProctoringFinalizationDialog_StatusWaiting">
Waiting to resume %%_COUNT_%% transmission operations at %%_TIME_%%...
</Entry>
<Entry key="ProctoringFinalizationDialog_Title">
Screen Proctoring Finalization
</Entry>
<Entry key="RuntimeWindow_ApplicationRunning"> <Entry key="RuntimeWindow_ApplicationRunning">
SEB is running. SEB is running.
</Entry> </Entry>

View file

@ -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
{
/// <summary>
/// The event arguments used for the remaining work updated event fired by the <see cref="IProctoringController"/>.
/// </summary>
public class RemainingWorkUpdatedEventArgs
{
/// <summary>
/// The path of the local cache, if <see cref="HasFailed"/> is <c>true</c>.
/// </summary>
public string CachePath { get; set; }
/// <summary>
/// Indicates that the execution of the remaining work has failed.
/// </summary>
public bool HasFailed { get; set; }
/// <summary>
/// Indicates that the execution of the remaining work has finished.
/// </summary>
public bool IsFinished { get; set; }
/// <summary>
/// Indicates that the execution is paused resp. waiting to be resumed.
/// </summary>
public bool IsWaiting { get; set; }
/// <summary>
/// The point in time when the next transmission will take place, if available.
/// </summary>
public DateTime? Next { get; set; }
/// <summary>
/// The number of already executed work items.
/// </summary>
public int Progress { get; set; }
/// <summary>
/// The point in time when the execution will resume, if <see cref="IsWaiting"/> is <c>true</c>.
/// </summary>
public DateTime Resume { get; set; }
/// <summary>
/// The total number of work items to be executed.
/// </summary>
public int Total { get; set; }
}
}

View file

@ -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
{
/// <summary>
/// Event handler used to indicate that the remaining work status has been updated.
/// </summary>
public delegate void RemainingWorkUpdatedEventHandler(RemainingWorkUpdatedEventArgs args);
}

View file

@ -38,6 +38,21 @@ namespace SafeExamBrowser.Proctoring.Contracts
/// </summary> /// </summary>
event ProctoringEventHandler HandRaised; event ProctoringEventHandler HandRaised;
/// <summary>
/// Event fired when the status of the remaining work has been updated.
/// </summary>
event RemainingWorkUpdatedEventHandler RemainingWorkUpdated;
/// <summary>
/// Executes any remaining work like e.g. the transmission of cached screen shots. Make sure to do so before calling <see cref="Terminate"/>.
/// </summary>
void ExecuteRemainingWork();
/// <summary>
/// Indicates whether there is any remaining work which needs to be done before the proctoring can be terminated.
/// </summary>
bool HasRemainingWork();
/// <summary> /// <summary>
/// Initializes the given settings and starts the proctoring if the settings are valid. /// Initializes the given settings and starts the proctoring if the settings are valid.
/// </summary> /// </summary>
@ -54,7 +69,7 @@ namespace SafeExamBrowser.Proctoring.Contracts
void RaiseHand(string message = default); void RaiseHand(string message = default);
/// <summary> /// <summary>
/// Stops the proctoring functionality. /// Stops the proctoring functionality. Make sure to call <see cref="ExecuteRemainingWork"/> beforehand if necessary.
/// </summary> /// </summary>
void Terminate(); void Terminate();
} }

View file

@ -56,6 +56,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Events\ProctoringEventHandler.cs" /> <Compile Include="Events\ProctoringEventHandler.cs" />
<Compile Include="Events\RemainingWorkUpdatedEventArgs.cs" />
<Compile Include="Events\RemainingWorkUpdatedEventHandler.cs" />
<Compile Include="IProctoringController.cs" /> <Compile Include="IProctoringController.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup> </ItemGroup>

View file

@ -13,7 +13,6 @@ using System.Threading;
using System.Windows; using System.Windows;
using Microsoft.Web.WebView2.Wpf; using Microsoft.Web.WebView2.Wpf;
using SafeExamBrowser.Configuration.Contracts; using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Core.Contracts.Notifications.Events;
using SafeExamBrowser.Core.Contracts.Resources.Icons; using SafeExamBrowser.Core.Contracts.Resources.Icons;
using SafeExamBrowser.I18n.Contracts; using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.Logging.Contracts;
@ -41,8 +40,6 @@ namespace SafeExamBrowser.Proctoring.JitsiMeet
internal override string Name => nameof(JitsiMeet); internal override string Name => nameof(JitsiMeet);
public override event NotificationChangedEventHandler NotificationChanged;
internal JitsiMeetImplementation( internal JitsiMeetImplementation(
AppConfig appConfig, AppConfig appConfig,
IFileSystem fileSystem, IFileSystem fileSystem,
@ -194,7 +191,6 @@ namespace SafeExamBrowser.Proctoring.JitsiMeet
internal override void Terminate() internal override void Terminate()
{ {
Stop(); Stop();
TerminateNotification();
logger.Info("Terminated proctoring."); 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) private string LoadContent(ProctoringSettings settings)
{ {
var assembly = Assembly.GetAssembly(typeof(ProctoringController)); 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") }; IconResource = new XamlIconResource { Uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/Proctoring_Active.xaml") };
Tooltip = text.Get(TextKey.Notification_ProctoringActiveTooltip); Tooltip = text.Get(TextKey.Notification_ProctoringActiveTooltip);
NotificationChanged?.Invoke(); InvokeNotificationChanged();
} }
private void ShowNotificationInactive() 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") }; IconResource = new XamlIconResource { Uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/Proctoring_Inactive.xaml") };
Tooltip = text.Get(TextKey.Notification_ProctoringInactiveTooltip); Tooltip = text.Get(TextKey.Notification_ProctoringInactiveTooltip);
NotificationChanged?.Invoke(); InvokeNotificationChanged();
} }
} }
} }

View file

@ -8,6 +8,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using KGySoft.CoreLibraries;
using SafeExamBrowser.Browser.Contracts; using SafeExamBrowser.Browser.Contracts;
using SafeExamBrowser.Configuration.Contracts; using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Core.Contracts.Notifications; using SafeExamBrowser.Core.Contracts.Notifications;
@ -38,6 +39,11 @@ namespace SafeExamBrowser.Proctoring
public event ProctoringEventHandler HandLowered; public event ProctoringEventHandler HandLowered;
public event ProctoringEventHandler HandRaised; public event ProctoringEventHandler HandRaised;
public event RemainingWorkUpdatedEventHandler RemainingWorkUpdated
{
add { implementations.ForEach(i => i.RemainingWorkUpdated += value); }
remove { implementations.ForEach(i => i.RemainingWorkUpdated -= value); }
}
public ProctoringController( public ProctoringController(
AppConfig appConfig, AppConfig appConfig,
@ -57,6 +63,43 @@ namespace SafeExamBrowser.Proctoring
implementations = new List<ProctoringImplementation>(); implementations = new List<ProctoringImplementation>();
} }
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) public void Initialize(ProctoringSettings settings)
{ {
implementations = factory.CreateAllActive(settings); implementations = factory.CreateAllActive(settings);

View file

@ -9,6 +9,7 @@
using SafeExamBrowser.Core.Contracts.Notifications; using SafeExamBrowser.Core.Contracts.Notifications;
using SafeExamBrowser.Core.Contracts.Notifications.Events; using SafeExamBrowser.Core.Contracts.Notifications.Events;
using SafeExamBrowser.Core.Contracts.Resources.Icons; using SafeExamBrowser.Core.Contracts.Resources.Icons;
using SafeExamBrowser.Proctoring.Contracts.Events;
using SafeExamBrowser.Server.Contracts.Events.Proctoring; using SafeExamBrowser.Server.Contracts.Events.Proctoring;
namespace SafeExamBrowser.Proctoring namespace SafeExamBrowser.Proctoring
@ -21,7 +22,8 @@ namespace SafeExamBrowser.Proctoring
public string Tooltip { get; protected set; } public string Tooltip { get; protected set; }
public IconResource IconResource { 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() void INotification.Activate()
{ {
@ -40,7 +42,20 @@ namespace SafeExamBrowser.Proctoring
internal abstract void Stop(); internal abstract void Stop();
internal abstract void Terminate(); internal abstract void Terminate();
internal virtual void ExecuteRemainingWork() { }
internal virtual bool HasRemainingWork() => false;
protected virtual void ActivateNotification() { } protected virtual void ActivateNotification() { }
protected virtual void TerminateNotification() { } protected virtual void TerminateNotification() { }
protected void InvokeNotificationChanged()
{
NotificationChanged?.Invoke();
}
protected void InvokeRemainingWorkUpdated(RemainingWorkUpdatedEventArgs args)
{
RemainingWorkUpdated?.Invoke(args);
}
} }
} }

View file

@ -93,6 +93,7 @@
<Compile Include="ProctoringFactory.cs" /> <Compile Include="ProctoringFactory.cs" />
<Compile Include="ProctoringImplementation.cs" /> <Compile Include="ProctoringImplementation.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ScreenProctoring\Buffer.cs" />
<Compile Include="ScreenProctoring\Cache.cs" /> <Compile Include="ScreenProctoring\Cache.cs" />
<Compile Include="ScreenProctoring\Data\IntervalTrigger.cs" /> <Compile Include="ScreenProctoring\Data\IntervalTrigger.cs" />
<Compile Include="ScreenProctoring\Data\KeyboardTrigger.cs" /> <Compile Include="ScreenProctoring\Data\KeyboardTrigger.cs" />

View file

@ -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;
}
}
}
}

View file

@ -7,7 +7,7 @@
*/ */
using System; using System;
using System.Collections.Generic; using System.Collections.Concurrent;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Security.Cryptography; using System.Security.Cryptography;
@ -27,20 +27,21 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
private readonly AppConfig appConfig; private readonly AppConfig appConfig;
private readonly ILogger logger; 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) public Cache(AppConfig appConfig, ILogger logger)
{ {
this.appConfig = appConfig; this.appConfig = appConfig;
this.logger = logger; this.logger = logger;
this.queue = new Queue<(string, int, string)>(); this.queue = new ConcurrentQueue<(string, int, string)>();
} }
internal bool Any() internal bool Any()
{ {
return false; return queue.Any();
} }
internal bool TryEnqueue(MetaData metaData, ScreenShot screenShot) internal bool TryEnqueue(MetaData metaData, ScreenShot screenShot)
@ -57,7 +58,7 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
success = true; success = true;
logger.Debug($"Cached data for '{fileName}'."); logger.Debug($"Cached data for '{fileName}', now holding {Count} item(s).");
} }
catch (Exception e) catch (Exception e)
{ {
@ -74,23 +75,21 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
metaData = default; metaData = default;
screenShot = default; screenShot = default;
if (queue.Any()) if (queue.Any() && queue.TryPeek(out var item))
{ {
var (fileName, checksum, hash) = queue.Peek();
try try
{ {
LoadData(fileName, out metaData, out screenShot); LoadData(item.fileName, out metaData, out screenShot);
LoadImage(fileName, screenShot); LoadImage(item.fileName, screenShot);
Dequeue(fileName, checksum, hash, metaData, screenShot); Dequeue(item.fileName, item.checksum, item.hash, metaData, screenShot);
success = true; success = true;
logger.Debug($"Uncached data for '{fileName}'."); logger.Debug($"Removed data for '{item.fileName}', {Count} item(s) remaining.");
} }
catch (Exception e) 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(dataPath);
File.Delete(imagePath); File.Delete(imagePath);
queue.Dequeue(); while (!queue.TryDequeue(out _)) ;
} }
private void Enqueue(string fileName, MetaData metaData, ScreenShot screenShot) private void Enqueue(string fileName, MetaData metaData, ScreenShot screenShot)

View file

@ -9,7 +9,6 @@
using System; using System;
using SafeExamBrowser.Browser.Contracts; using SafeExamBrowser.Browser.Contracts;
using SafeExamBrowser.Configuration.Contracts; using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Core.Contracts.Notifications.Events;
using SafeExamBrowser.Core.Contracts.Resources.Icons; using SafeExamBrowser.Core.Contracts.Resources.Icons;
using SafeExamBrowser.I18n.Contracts; using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.Logging.Contracts;
@ -34,8 +33,6 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
internal override string Name => nameof(ScreenProctoring); internal override string Name => nameof(ScreenProctoring);
public override event NotificationChangedEventHandler NotificationChanged;
internal ScreenProctoringImplementation( internal ScreenProctoringImplementation(
AppConfig appConfig, AppConfig appConfig,
IApplicationMonitor applicationMonitor, IApplicationMonitor applicationMonitor,
@ -54,6 +51,29 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
this.text = text; 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() internal override void Initialize()
{ {
var start = true; var start = true;
@ -132,10 +152,7 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
internal override void Terminate() internal override void Terminate()
{ {
// TODO: Cache transmission or user information!
Stop(); Stop();
TerminateNotification();
logger.Info("Terminated proctoring."); logger.Info("Terminated proctoring.");
} }
@ -189,7 +206,7 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
Tooltip = text.Get(TextKey.Notification_ProctoringInactiveTooltip); Tooltip = text.Get(TextKey.Notification_ProctoringInactiveTooltip);
} }
NotificationChanged?.Invoke(); InvokeNotificationChanged();
} }
} }
} }

View file

@ -8,12 +8,11 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
using System.Timers; using System.Timers;
using SafeExamBrowser.Configuration.Contracts; using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Proctoring.Contracts.Events;
using SafeExamBrowser.Proctoring.ScreenProctoring.Data; using SafeExamBrowser.Proctoring.ScreenProctoring.Data;
using SafeExamBrowser.Proctoring.ScreenProctoring.Imaging; using SafeExamBrowser.Proctoring.ScreenProctoring.Imaging;
using SafeExamBrowser.Proctoring.ScreenProctoring.Service; using SafeExamBrowser.Proctoring.ScreenProctoring.Service;
@ -23,9 +22,10 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
{ {
internal class TransmissionSpooler internal class TransmissionSpooler
{ {
const int BAD = 10; private const int BAD = 10;
const int GOOD = 0; private const int GOOD = 0;
private readonly Buffer buffer;
private readonly Cache cache; private readonly Cache cache;
private readonly ILogger logger; private readonly ILogger logger;
private readonly ConcurrentQueue<(MetaData metaData, ScreenShot screenShot)> queue; private readonly ConcurrentQueue<(MetaData metaData, ScreenShot screenShot)> queue;
@ -33,8 +33,8 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
private readonly ServiceProxy service; private readonly ServiceProxy service;
private readonly Timer timer; private readonly Timer timer;
private Queue<(MetaData metaData, DateTime schedule, ScreenShot screenShot)> buffer;
private int health; private int health;
private bool networkIssue;
private bool recovering; private bool recovering;
private DateTime resume; private DateTime resume;
private Thread thread; private Thread thread;
@ -42,7 +42,7 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
internal TransmissionSpooler(AppConfig appConfig, IModuleLogger logger, ServiceProxy service) 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.cache = new Cache(appConfig, logger.CloneFor(nameof(Cache)));
this.logger = logger; this.logger = logger;
this.queue = new ConcurrentQueue<(MetaData, ScreenShot)>(); this.queue = new ConcurrentQueue<(MetaData, ScreenShot)>();
@ -56,6 +56,55 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
queue.Enqueue((metaData, screenShot)); queue.Enqueue((metaData, screenShot));
} }
internal void ExecuteRemainingWork(Action<RemainingWorkUpdatedEventArgs> 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() internal void Start()
{ {
const int FIFTEEN_SECONDS = 15000; const int FIFTEEN_SECONDS = 15000;
@ -167,14 +216,14 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
{ {
BufferFromCache(); BufferFromCache();
BufferFromQueue(); BufferFromQueue();
TryTransmitFromBuffer(); TransmitFromBuffer();
} }
private void ExecuteNormally() private void ExecuteNormally()
{ {
TryTransmitFromBuffer(); TransmitFromBuffer();
TryTransmitFromCache(); TransmitFromCache();
TryTransmitFromQueue(); TransmitFromQueue();
} }
private void ExecuteRecovery() 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() private void BufferFromCache()
{ {
if (cache.TryDequeue(out var metaData, out var screenShot)) 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)) if (TryDequeue(out var metaData, out var screenShot))
{ {
Buffer(metaData, CalculateSchedule(metaData), screenShot); buffer.Enqueue(metaData, CalculateSchedule(metaData), screenShot);
} }
} }
private void CacheFromBuffer() 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); buffer.Dequeue();
screenShot.Dispose();
if (success)
{
buffer.Dequeue();
screenShot.Dispose();
}
} }
} }
@ -234,15 +272,13 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
{ {
if (TryDequeue(out var metaData, out var screenShot)) if (TryDequeue(out var metaData, out var screenShot))
{ {
var success = cache.TryEnqueue(metaData, screenShot); if (cache.TryEnqueue(metaData, screenShot))
if (success)
{ {
screenShot.Dispose(); screenShot.Dispose();
} }
else else
{ {
Buffer(metaData, DateTime.Now, screenShot); buffer.Enqueue(metaData, DateTime.Now, screenShot);
} }
} }
} }
@ -255,6 +291,48 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
return schedule; 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) private bool TryDequeue(out MetaData metaData, out ScreenShot screenShot)
{ {
metaData = default; metaData = default;
@ -269,80 +347,6 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
return metaData != default && screenShot != default; 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) private bool TryTransmit(MetaData metaData, ScreenShot screenShot)
{ {
var success = false; var success = false;
@ -351,10 +355,12 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
{ {
var response = service.Send(metaData, screenShot); var response = service.Send(metaData, screenShot);
networkIssue = !response.Success;
success = response.Success;
if (response.Success) if (response.Success)
{ {
health = UpdateHealth(response.Value); health = UpdateHealth(response.Value);
success = true;
} }
} }
else else
@ -371,6 +377,8 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
{ {
var response = service.GetHealth(); var response = service.GetHealth();
networkIssue = !response.Success;
if (response.Success) if (response.Success)
{ {
health = UpdateHealth(response.Value); health = UpdateHealth(response.Value);

View file

@ -103,6 +103,11 @@ namespace SafeExamBrowser.UserInterface.Contracts
/// </summary> /// </summary>
ISystemControl CreatePowerSupplyControl(IPowerSupply powerSupply, Location location); ISystemControl CreatePowerSupplyControl(IPowerSupply powerSupply, Location location);
/// <summary>
/// Creates a new dialog to display the status of the proctoring finalization.
/// </summary>
IProctoringFinalizationDialog CreateProctoringFinalizationDialog();
/// <summary> /// <summary>
/// Creates a new proctoring window loaded with the given proctoring control. /// Creates a new proctoring window loaded with the given proctoring control.
/// </summary> /// </summary>

View file

@ -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
{
/// <summary>
/// The dialog to display the status of the proctoring finalization.
/// </summary>
public interface IProctoringFinalizationDialog
{
/// <summary>
/// Shows the dialog as topmost window.
/// </summary>
void Show();
/// <summary>
/// Updates the status of the finalization.
/// </summary>
void Update(RemainingWorkUpdatedEventArgs status);
}
}

View file

@ -103,6 +103,7 @@
<Compile Include="Windows\ILockScreen.cs" /> <Compile Include="Windows\ILockScreen.cs" />
<Compile Include="Windows\IPasswordDialog.cs" /> <Compile Include="Windows\IPasswordDialog.cs" />
<Compile Include="Windows\Data\PasswordDialogResult.cs" /> <Compile Include="Windows\Data\PasswordDialogResult.cs" />
<Compile Include="Proctoring\IProctoringFinalizationDialog.cs" />
<Compile Include="Windows\IRuntimeWindow.cs" /> <Compile Include="Windows\IRuntimeWindow.cs" />
<Compile Include="Windows\IServerFailureDialog.cs" /> <Compile Include="Windows\IServerFailureDialog.cs" />
<Compile Include="Windows\ISplashScreen.cs" /> <Compile Include="Windows\ISplashScreen.cs" />

View file

@ -171,6 +171,9 @@
<Compile Include="Windows\PasswordDialog.xaml.cs"> <Compile Include="Windows\PasswordDialog.xaml.cs">
<DependentUpon>PasswordDialog.xaml</DependentUpon> <DependentUpon>PasswordDialog.xaml</DependentUpon>
</Compile> </Compile>
<Compile Include="Windows\ProctoringFinalizationDialog.xaml.cs">
<DependentUpon>ProctoringFinalizationDialog.xaml</DependentUpon>
</Compile>
<Compile Include="Windows\ProctoringWindow.xaml.cs"> <Compile Include="Windows\ProctoringWindow.xaml.cs">
<DependentUpon>ProctoringWindow.xaml</DependentUpon> <DependentUpon>ProctoringWindow.xaml</DependentUpon>
</Compile> </Compile>
@ -391,6 +394,10 @@
<SubType>Designer</SubType> <SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
<Page Include="Windows\ProctoringFinalizationDialog.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Windows\ProctoringWindow.xaml"> <Page Include="Windows\ProctoringWindow.xaml">
<SubType>Designer</SubType> <SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>

View file

@ -177,6 +177,11 @@ namespace SafeExamBrowser.UserInterface.Desktop
} }
} }
public IProctoringFinalizationDialog CreateProctoringFinalizationDialog()
{
return Application.Current.Dispatcher.Invoke(() => new ProctoringFinalizationDialog(text));
}
public IProctoringWindow CreateProctoringWindow(IProctoringControl control) public IProctoringWindow CreateProctoringWindow(IProctoringControl control)
{ {
return Application.Current.Dispatcher.Invoke(() => new ProctoringWindow(control)); return Application.Current.Dispatcher.Invoke(() => new ProctoringWindow(control));

View file

@ -0,0 +1,53 @@
<Window x:Class="SafeExamBrowser.UserInterface.Desktop.Windows.ProctoringFinalizationDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:fa="http://schemas.fontawesome.io/icons/"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Desktop.Windows"
mc:Ignorable="d" Height="250" Width="650" ResizeMode="NoResize" Topmost="True" WindowStartupLocation="CenterScreen">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Templates/Colors.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="20">
<Grid Name="ProgressPanel">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Name="Info" TextWrapping="WrapWithOverflow" VerticalAlignment="Bottom" />
<Grid Grid.Row="1">
<ProgressBar Name="Progress" Height="25" Margin="0,20" />
<TextBlock Name="Percentage" FontWeight="Bold" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
<TextBlock Grid.Row="2" Name="Status" FontStyle="Italic" TextWrapping="WrapWithOverflow" VerticalAlignment="Top" />
</Grid>
<Grid Name="FailurePanel">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<fa:ImageAwesome Grid.Column="0" Foreground="LightGray" Icon="Warning" Margin="10,0,20,0" Width="50" />
<WrapPanel Grid.Column="1" Orientation="Vertical" VerticalAlignment="Center">
<TextBlock Name="Message" TextWrapping="WrapWithOverflow" />
<TextBlock Name="CachePath" FontFamily="Courier New" Margin="0,20,0,0" TextWrapping="WrapWithOverflow" />
</WrapPanel>
</Grid>
</Grid>
<Grid Grid.Row="1" Name="ButtonPanel" Background="{StaticResource BackgroundBrush}">
<WrapPanel Orientation="Horizontal" Margin="20" HorizontalAlignment="Right" VerticalAlignment="Center">
<Button Name="Button" Cursor="Hand" Padding="10,5" MinWidth="75" />
</WrapPanel>
</Grid>
</Grid>
</Window>

View file

@ -0,0 +1,114 @@
/*
* 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.Windows;
using System.Windows.Input;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Proctoring.Contracts.Events;
using SafeExamBrowser.UserInterface.Contracts.Proctoring;
using SafeExamBrowser.UserInterface.Shared.Utilities;
namespace SafeExamBrowser.UserInterface.Desktop.Windows
{
public partial class ProctoringFinalizationDialog : Window, IProctoringFinalizationDialog
{
private readonly IText text;
public ProctoringFinalizationDialog(IText text)
{
this.text = text;
InitializeComponent();
InitializeDialog();
}
public new void Show()
{
Dispatcher.Invoke(() => ShowDialog());
}
public void Update(RemainingWorkUpdatedEventArgs status)
{
Dispatcher.Invoke(() =>
{
if (status.HasFailed)
{
ShowFailure(status);
}
else if (status.IsFinished)
{
Close();
}
else
{
ShowProgress(status);
}
});
}
private void InitializeDialog()
{
Button.Click += (o, args) => Close();
Button.Content = text.Get(TextKey.ProctoringFinalizationDialog_Confirm);
Loaded += (o, args) => this.DisableCloseButton();
Title = text.Get(TextKey.ProctoringFinalizationDialog_Title);
}
private void ShowFailure(RemainingWorkUpdatedEventArgs status)
{
ButtonPanel.Visibility = Visibility.Visible;
CachePath.Text = status.CachePath;
Cursor = Cursors.Arrow;
FailurePanel.Visibility = Visibility.Visible;
Message.Text = text.Get(TextKey.ProctoringFinalizationDialog_FailureMessage);
ProgressPanel.Visibility = Visibility.Collapsed;
this.EnableCloseButton();
}
private void ShowProgress(RemainingWorkUpdatedEventArgs status)
{
ButtonPanel.Visibility = Visibility.Collapsed;
Cursor = Cursors.Wait;
FailurePanel.Visibility = Visibility.Collapsed;
Info.Text = text.Get(TextKey.ProctoringFinalizationDialog_InfoMessage);
ProgressPanel.Visibility = Visibility.Visible;
if (status.IsWaiting)
{
var count = $"{status.Total - status.Progress}";
var time = $"{status.Resume.ToLongTimeString()}";
Percentage.Text = "";
Progress.IsIndeterminate = true;
Status.Text = text.Get(TextKey.ProctoringFinalizationDialog_StatusWaiting).Replace("%%_COUNT_%%", count).Replace("%%_TIME_%%", time);
}
else
{
var count = $"{status.Progress}";
var total = $"{status.Total}";
Percentage.Text = $"{status.Progress / (double) (status.Total > 0 ? status.Total : 1) * 100:N0}%";
Progress.IsIndeterminate = false;
Progress.Maximum = status.Total;
Progress.Value = status.Progress;
if (status.Next.HasValue)
{
Status.Text = text.Get(TextKey.ProctoringFinalizationDialog_StatusAndTime).Replace("%%_TIME_%%", $"{status.Next.Value.ToLongTimeString()}");
}
else
{
Status.Text = text.Get(TextKey.ProctoringFinalizationDialog_Status);
}
Status.Text = Status.Text.Replace("%%_COUNT_%%", count).Replace("%%_TOTAL_%%", total);
}
}
}
}

View file

@ -179,6 +179,9 @@
<Compile Include="Properties\AssemblyInfo.cs"> <Compile Include="Properties\AssemblyInfo.cs">
<SubType>Code</SubType> <SubType>Code</SubType>
</Compile> </Compile>
<Compile Include="Windows\ProctoringFinalizationDialog.xaml.cs">
<DependentUpon>ProctoringFinalizationDialog.xaml</DependentUpon>
</Compile>
<Compile Include="Windows\ProctoringWindow.xaml.cs"> <Compile Include="Windows\ProctoringWindow.xaml.cs">
<DependentUpon>ProctoringWindow.xaml</DependentUpon> <DependentUpon>ProctoringWindow.xaml</DependentUpon>
</Compile> </Compile>
@ -522,6 +525,10 @@
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType> <SubType>Designer</SubType>
</Page> </Page>
<Page Include="Windows\ProctoringFinalizationDialog.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Windows\ProctoringWindow.xaml"> <Page Include="Windows\ProctoringWindow.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType> <SubType>Designer</SubType>

View file

@ -177,6 +177,11 @@ namespace SafeExamBrowser.UserInterface.Mobile
} }
} }
public IProctoringFinalizationDialog CreateProctoringFinalizationDialog()
{
return Application.Current.Dispatcher.Invoke(() => new ProctoringFinalizationDialog(text));
}
public IProctoringWindow CreateProctoringWindow(IProctoringControl control) public IProctoringWindow CreateProctoringWindow(IProctoringControl control)
{ {
return Application.Current.Dispatcher.Invoke(() => new ProctoringWindow(control)); return Application.Current.Dispatcher.Invoke(() => new ProctoringWindow(control));

View file

@ -0,0 +1,56 @@
<Window x:Class="SafeExamBrowser.UserInterface.Mobile.Windows.ProctoringFinalizationDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:fa="http://schemas.fontawesome.io/icons/"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Mobile.Windows"
mc:Ignorable="d" Background="Transparent" Height="750" Width="1000" FontSize="16" ResizeMode="NoResize" Topmost="True" AllowsTransparency="True" WindowStyle="None">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Templates/Colors.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid Background="#66000000">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" />
<Grid Grid.Row="1" Background="White">
<Grid Name="ProgressPanel" Margin="50">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Name="Info" TextWrapping="WrapWithOverflow" VerticalAlignment="Bottom" />
<Grid Grid.Row="1">
<ProgressBar Name="Progress" Height="35" Margin="0,25" />
<TextBlock Name="Percentage" FontWeight="Bold" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
<TextBlock Grid.Row="2" Name="Status" FontStyle="Italic" TextWrapping="WrapWithOverflow" VerticalAlignment="Top" />
</Grid>
<Grid Name="FailurePanel" Margin="50">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<fa:ImageAwesome Grid.Column="0" Foreground="LightGray" Icon="Warning" Margin="10,0,50,0" Width="50" />
<WrapPanel Grid.Column="1" Orientation="Vertical" VerticalAlignment="Center">
<TextBlock Name="Message" TextWrapping="WrapWithOverflow" />
<TextBlock Name="CachePath" FontFamily="Courier New" Margin="0,25,0,0" TextWrapping="WrapWithOverflow" />
</WrapPanel>
</Grid>
</Grid>
<Grid Grid.Row="2" Name="ButtonPanel" Background="{StaticResource BackgroundBrush}">
<WrapPanel Orientation="Horizontal" Margin="50,25" HorizontalAlignment="Right" VerticalAlignment="Center">
<Button Name="Button" Cursor="Hand" Padding="10,5" MinWidth="75" />
</WrapPanel>
</Grid>
</Grid>
</Window>

View file

@ -0,0 +1,135 @@
/*
* 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.ComponentModel;
using System.Windows;
using System.Windows.Input;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Proctoring.Contracts.Events;
using SafeExamBrowser.UserInterface.Contracts.Proctoring;
namespace SafeExamBrowser.UserInterface.Mobile.Windows
{
public partial class ProctoringFinalizationDialog : Window, IProctoringFinalizationDialog
{
private readonly IText text;
public ProctoringFinalizationDialog(IText text)
{
this.text = text;
InitializeComponent();
InitializeDialog();
}
public new void Show()
{
Dispatcher.Invoke(() =>
{
InitializeBounds();
ShowDialog();
});
}
public void Update(RemainingWorkUpdatedEventArgs status)
{
Dispatcher.Invoke(() =>
{
if (status.HasFailed)
{
ShowFailure(status);
}
else if (status.IsFinished)
{
Close();
}
else
{
ShowProgress(status);
}
});
}
private void InitializeBounds()
{
Left = 0;
Top = 0;
Height = SystemParameters.PrimaryScreenHeight;
Width = SystemParameters.PrimaryScreenWidth;
}
private void InitializeDialog()
{
Button.Click += (o, args) => Close();
Button.Content = text.Get(TextKey.ProctoringFinalizationDialog_Confirm);
Title = text.Get(TextKey.ProctoringFinalizationDialog_Title);
InitializeBounds();
SystemParameters.StaticPropertyChanged += SystemParameters_StaticPropertyChanged;
}
private void ShowFailure(RemainingWorkUpdatedEventArgs status)
{
ButtonPanel.Visibility = Visibility.Visible;
CachePath.Text = status.CachePath;
Cursor = Cursors.Arrow;
FailurePanel.Visibility = Visibility.Visible;
Message.Text = text.Get(TextKey.ProctoringFinalizationDialog_FailureMessage);
ProgressPanel.Visibility = Visibility.Collapsed;
}
private void ShowProgress(RemainingWorkUpdatedEventArgs status)
{
ButtonPanel.Visibility = Visibility.Collapsed;
Cursor = Cursors.Wait;
FailurePanel.Visibility = Visibility.Collapsed;
Info.Text = text.Get(TextKey.ProctoringFinalizationDialog_InfoMessage);
ProgressPanel.Visibility = Visibility.Visible;
if (status.IsWaiting)
{
var count = $"{status.Total - status.Progress}";
var time = $"{status.Resume.ToLongTimeString()}";
Percentage.Text = "";
Progress.IsIndeterminate = true;
Status.Text = text.Get(TextKey.ProctoringFinalizationDialog_StatusWaiting).Replace("%%_COUNT_%%", count).Replace("%%_TIME_%%", time);
}
else
{
var count = $"{status.Progress}";
var total = $"{status.Total}";
Percentage.Text = $"{status.Progress / (double) (status.Total > 0 ? status.Total : 1) * 100:N0}%";
Progress.IsIndeterminate = false;
Progress.Maximum = status.Total;
Progress.Value = status.Progress;
if (status.Next.HasValue)
{
Status.Text = text.Get(TextKey.ProctoringFinalizationDialog_StatusAndTime).Replace("%%_TIME_%%", $"{status.Next.Value.ToLongTimeString()}");
}
else
{
Status.Text = text.Get(TextKey.ProctoringFinalizationDialog_Status);
}
Status.Text = Status.Text.Replace("%%_COUNT_%%", count).Replace("%%_TOTAL_%%", total);
}
}
private void SystemParameters_StaticPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(SystemParameters.WorkArea))
{
Dispatcher.InvokeAsync(InitializeBounds);
}
}
}
}

View file

@ -17,8 +17,8 @@ namespace SafeExamBrowser.UserInterface.Shared.Utilities
{ {
private const int GWL_STYLE = -16; private const int GWL_STYLE = -16;
private const uint MF_BYCOMMAND = 0x00000000; private const uint MF_BYCOMMAND = 0x00000000;
private const uint MF_GRAYED = 0x00000001;
private const uint MF_ENABLED = 0x00000000; private const uint MF_ENABLED = 0x00000000;
private const uint MF_GRAYED = 0x00000001;
private const uint SC_CLOSE = 0xF060; private const uint SC_CLOSE = 0xF060;
private const uint SWP_SHOWWINDOW = 0x0040; private const uint SWP_SHOWWINDOW = 0x0040;
private const int WS_SYSMENU = 0x80000; private const int WS_SYSMENU = 0x80000;
@ -36,6 +36,17 @@ namespace SafeExamBrowser.UserInterface.Shared.Utilities
} }
} }
public static void EnableCloseButton(this Window window)
{
var helper = new WindowInteropHelper(window);
var systemMenu = GetSystemMenu(helper.Handle, false);
if (systemMenu != IntPtr.Zero)
{
EnableMenuItem(systemMenu, SC_CLOSE, MF_BYCOMMAND | MF_ENABLED);
}
}
public static void HideCloseButton(this Window window) public static void HideCloseButton(this Window window)
{ {
var helper = new WindowInteropHelper(window); var helper = new WindowInteropHelper(window);