SEBSP-107: Implemented screen proctoring finalization.
This commit is contained in:
parent
787c84cc0e
commit
ff5b91c010
30 changed files with 927 additions and 155 deletions
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
92
SafeExamBrowser.Proctoring/ScreenProctoring/Buffer.cs
Normal file
92
SafeExamBrowser.Proctoring/ScreenProctoring/Buffer.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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" />
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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>
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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>
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in a new issue