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.Configuration.Contracts;
|
||||
using SafeExamBrowser.Configuration.Contracts.Integrity;
|
||||
using SafeExamBrowser.Proctoring.Contracts;
|
||||
using SafeExamBrowser.Server.Contracts;
|
||||
using SafeExamBrowser.Settings;
|
||||
using SafeExamBrowser.UserInterface.Contracts.Shell;
|
||||
|
@ -54,6 +55,11 @@ namespace SafeExamBrowser.Client
|
|||
/// </summary>
|
||||
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>
|
||||
/// The server proxy to be used if the current session mode is <see cref="SessionMode.Server"/>.
|
||||
/// </summary>
|
||||
|
|
|
@ -30,6 +30,8 @@ using SafeExamBrowser.Logging.Contracts;
|
|||
using SafeExamBrowser.Monitoring.Contracts.Applications;
|
||||
using SafeExamBrowser.Monitoring.Contracts.Display;
|
||||
using SafeExamBrowser.Monitoring.Contracts.System;
|
||||
using SafeExamBrowser.Proctoring.Contracts;
|
||||
using SafeExamBrowser.Proctoring.Contracts.Events;
|
||||
using SafeExamBrowser.Server.Contracts;
|
||||
using SafeExamBrowser.Server.Contracts.Data;
|
||||
using SafeExamBrowser.Settings;
|
||||
|
@ -68,6 +70,7 @@ namespace SafeExamBrowser.Client
|
|||
private IBrowserApplication Browser => context.Browser;
|
||||
private IClientHost ClientHost => context.ClientHost;
|
||||
private IIntegrityModule IntegrityModule => context.IntegrityModule;
|
||||
private IProctoringController Proctoring => context.Proctoring;
|
||||
private IServerProxy Server => context.Server;
|
||||
private AppSettings Settings => context.Settings;
|
||||
|
||||
|
@ -230,16 +233,6 @@ namespace SafeExamBrowser.Client
|
|||
}
|
||||
}
|
||||
|
||||
private void Taskbar_LoseFocusRequested(bool forward)
|
||||
{
|
||||
Browser.Focus(forward);
|
||||
}
|
||||
|
||||
private void Browser_LoseFocusRequested(bool forward)
|
||||
{
|
||||
taskbar.Focus(forward);
|
||||
}
|
||||
|
||||
private void DeregisterEvents()
|
||||
{
|
||||
actionCenter.QuitButtonClicked -= Shell_QuitButtonClicked;
|
||||
|
@ -249,6 +242,7 @@ namespace SafeExamBrowser.Client
|
|||
registry.ValueChanged -= Registry_ValueChanged;
|
||||
runtime.ConnectionLost -= Runtime_ConnectionLost;
|
||||
systemMonitor.SessionChanged -= SystemMonitor_SessionChanged;
|
||||
taskbar.LoseFocusRequested -= Taskbar_LoseFocusRequested;
|
||||
taskbar.QuitButtonClicked -= Shell_QuitButtonClicked;
|
||||
|
||||
if (Browser != null)
|
||||
|
@ -327,6 +321,29 @@ namespace SafeExamBrowser.Client
|
|||
}
|
||||
}
|
||||
|
||||
private void PrepareShutdown()
|
||||
{
|
||||
FinalizeProctoring();
|
||||
}
|
||||
|
||||
private void FinalizeProctoring()
|
||||
{
|
||||
if (Proctoring != default && Proctoring.HasRemainingWork())
|
||||
{
|
||||
var dialog = uiFactory.CreateProctoringFinalizationDialog();
|
||||
var handler = new RemainingWorkUpdatedEventHandler((args) => dialog.Update(args));
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
Proctoring.RemainingWorkUpdated += handler;
|
||||
Proctoring.ExecuteRemainingWork();
|
||||
Proctoring.RemainingWorkUpdated -= handler;
|
||||
});
|
||||
|
||||
dialog.Show();
|
||||
}
|
||||
}
|
||||
|
||||
private void ScheduleIntegrityVerification()
|
||||
{
|
||||
const int FIVE_MINUTES = 300000;
|
||||
|
@ -510,6 +527,8 @@ namespace SafeExamBrowser.Client
|
|||
{
|
||||
if (success)
|
||||
{
|
||||
PrepareShutdown();
|
||||
|
||||
var communication = runtime.RequestReconfiguration(filePath, url);
|
||||
|
||||
if (communication.Success)
|
||||
|
@ -531,6 +550,11 @@ namespace SafeExamBrowser.Client
|
|||
}
|
||||
}
|
||||
|
||||
private void Browser_LoseFocusRequested(bool forward)
|
||||
{
|
||||
taskbar.Focus(forward);
|
||||
}
|
||||
|
||||
private void Browser_UserIdentifierDetected(string identifier)
|
||||
{
|
||||
if (Settings.SessionMode == SessionMode.Server)
|
||||
|
@ -876,6 +900,11 @@ namespace SafeExamBrowser.Client
|
|||
}
|
||||
}
|
||||
|
||||
private void Taskbar_LoseFocusRequested(bool forward)
|
||||
{
|
||||
Browser.Focus(forward);
|
||||
}
|
||||
|
||||
private void TerminationActivator_Activated()
|
||||
{
|
||||
PauseActivators();
|
||||
|
@ -1023,19 +1052,19 @@ namespace SafeExamBrowser.Client
|
|||
private bool TryInitiateShutdown()
|
||||
{
|
||||
var hasQuitPassword = !string.IsNullOrEmpty(Settings.Security.QuitPasswordHash);
|
||||
var requestShutdown = false;
|
||||
var initiateShutdown = false;
|
||||
var succes = false;
|
||||
|
||||
if (hasQuitPassword)
|
||||
{
|
||||
requestShutdown = TryValidateQuitPassword();
|
||||
initiateShutdown = TryValidateQuitPassword();
|
||||
}
|
||||
else
|
||||
{
|
||||
requestShutdown = TryConfirmShutdown();
|
||||
initiateShutdown = TryConfirmShutdown();
|
||||
}
|
||||
|
||||
if (requestShutdown)
|
||||
if (initiateShutdown)
|
||||
{
|
||||
succes = TryRequestShutdown();
|
||||
}
|
||||
|
@ -1084,6 +1113,8 @@ namespace SafeExamBrowser.Client
|
|||
|
||||
private bool TryRequestShutdown()
|
||||
{
|
||||
PrepareShutdown();
|
||||
|
||||
var communication = runtime.RequestShutdown();
|
||||
|
||||
if (!communication.Success)
|
||||
|
|
|
@ -301,6 +301,8 @@ namespace SafeExamBrowser.Client
|
|||
uiFactory);
|
||||
var operation = new ProctoringOperation(actionCenter, context, controller, logger, taskbar, uiFactory);
|
||||
|
||||
context.Proctoring = controller;
|
||||
|
||||
return operation;
|
||||
}
|
||||
|
||||
|
|
|
@ -232,6 +232,14 @@ namespace SafeExamBrowser.I18n.Contracts
|
|||
PasswordDialog_QuitPasswordRequiredTitle,
|
||||
PasswordDialog_SettingsPasswordRequired,
|
||||
PasswordDialog_SettingsPasswordRequiredTitle,
|
||||
// TODO: Translate text for ProctoringFinalizationDialog_... to remaining languages!
|
||||
ProctoringFinalizationDialog_Confirm,
|
||||
ProctoringFinalizationDialog_FailureMessage,
|
||||
ProctoringFinalizationDialog_InfoMessage,
|
||||
ProctoringFinalizationDialog_Status,
|
||||
ProctoringFinalizationDialog_StatusAndTime,
|
||||
ProctoringFinalizationDialog_StatusWaiting,
|
||||
ProctoringFinalizationDialog_Title,
|
||||
RuntimeWindow_ApplicationRunning,
|
||||
ServerFailureDialog_Abort,
|
||||
ServerFailureDialog_Fallback,
|
||||
|
|
|
@ -654,6 +654,27 @@
|
|||
<Entry key="PasswordDialog_SettingsPasswordRequiredTitle">
|
||||
Passwort erforderlich
|
||||
</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">
|
||||
SEB wird ausgeführt.
|
||||
</Entry>
|
||||
|
|
|
@ -654,6 +654,27 @@
|
|||
<Entry key="PasswordDialog_SettingsPasswordRequiredTitle">
|
||||
Password Required
|
||||
</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">
|
||||
SEB is running.
|
||||
</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>
|
||||
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>
|
||||
/// Initializes the given settings and starts the proctoring if the settings are valid.
|
||||
/// </summary>
|
||||
|
@ -54,7 +69,7 @@ namespace SafeExamBrowser.Proctoring.Contracts
|
|||
void RaiseHand(string message = default);
|
||||
|
||||
/// <summary>
|
||||
/// Stops the proctoring functionality.
|
||||
/// Stops the proctoring functionality. Make sure to call <see cref="ExecuteRemainingWork"/> beforehand if necessary.
|
||||
/// </summary>
|
||||
void Terminate();
|
||||
}
|
||||
|
|
|
@ -56,6 +56,8 @@
|
|||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Events\ProctoringEventHandler.cs" />
|
||||
<Compile Include="Events\RemainingWorkUpdatedEventArgs.cs" />
|
||||
<Compile Include="Events\RemainingWorkUpdatedEventHandler.cs" />
|
||||
<Compile Include="IProctoringController.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
|
|
|
@ -13,7 +13,6 @@ using System.Threading;
|
|||
using System.Windows;
|
||||
using Microsoft.Web.WebView2.Wpf;
|
||||
using SafeExamBrowser.Configuration.Contracts;
|
||||
using SafeExamBrowser.Core.Contracts.Notifications.Events;
|
||||
using SafeExamBrowser.Core.Contracts.Resources.Icons;
|
||||
using SafeExamBrowser.I18n.Contracts;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
|
@ -41,8 +40,6 @@ namespace SafeExamBrowser.Proctoring.JitsiMeet
|
|||
|
||||
internal override string Name => nameof(JitsiMeet);
|
||||
|
||||
public override event NotificationChangedEventHandler NotificationChanged;
|
||||
|
||||
internal JitsiMeetImplementation(
|
||||
AppConfig appConfig,
|
||||
IFileSystem fileSystem,
|
||||
|
@ -194,7 +191,6 @@ namespace SafeExamBrowser.Proctoring.JitsiMeet
|
|||
internal override void Terminate()
|
||||
{
|
||||
Stop();
|
||||
TerminateNotification();
|
||||
logger.Info("Terminated proctoring.");
|
||||
}
|
||||
|
||||
|
@ -210,11 +206,6 @@ namespace SafeExamBrowser.Proctoring.JitsiMeet
|
|||
}
|
||||
}
|
||||
|
||||
protected override void TerminateNotification()
|
||||
{
|
||||
// Nothing to do here for now.
|
||||
}
|
||||
|
||||
private string LoadContent(ProctoringSettings settings)
|
||||
{
|
||||
var assembly = Assembly.GetAssembly(typeof(ProctoringController));
|
||||
|
@ -249,7 +240,7 @@ namespace SafeExamBrowser.Proctoring.JitsiMeet
|
|||
IconResource = new XamlIconResource { Uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/Proctoring_Active.xaml") };
|
||||
Tooltip = text.Get(TextKey.Notification_ProctoringActiveTooltip);
|
||||
|
||||
NotificationChanged?.Invoke();
|
||||
InvokeNotificationChanged();
|
||||
}
|
||||
|
||||
private void ShowNotificationInactive()
|
||||
|
@ -258,7 +249,7 @@ namespace SafeExamBrowser.Proctoring.JitsiMeet
|
|||
IconResource = new XamlIconResource { Uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/Proctoring_Inactive.xaml") };
|
||||
Tooltip = text.Get(TextKey.Notification_ProctoringInactiveTooltip);
|
||||
|
||||
NotificationChanged?.Invoke();
|
||||
InvokeNotificationChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using KGySoft.CoreLibraries;
|
||||
using SafeExamBrowser.Browser.Contracts;
|
||||
using SafeExamBrowser.Configuration.Contracts;
|
||||
using SafeExamBrowser.Core.Contracts.Notifications;
|
||||
|
@ -38,6 +39,11 @@ namespace SafeExamBrowser.Proctoring
|
|||
|
||||
public event ProctoringEventHandler HandLowered;
|
||||
public event ProctoringEventHandler HandRaised;
|
||||
public event RemainingWorkUpdatedEventHandler RemainingWorkUpdated
|
||||
{
|
||||
add { implementations.ForEach(i => i.RemainingWorkUpdated += value); }
|
||||
remove { implementations.ForEach(i => i.RemainingWorkUpdated -= value); }
|
||||
}
|
||||
|
||||
public ProctoringController(
|
||||
AppConfig appConfig,
|
||||
|
@ -57,6 +63,43 @@ namespace SafeExamBrowser.Proctoring
|
|||
implementations = new List<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)
|
||||
{
|
||||
implementations = factory.CreateAllActive(settings);
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
using SafeExamBrowser.Core.Contracts.Notifications;
|
||||
using SafeExamBrowser.Core.Contracts.Notifications.Events;
|
||||
using SafeExamBrowser.Core.Contracts.Resources.Icons;
|
||||
using SafeExamBrowser.Proctoring.Contracts.Events;
|
||||
using SafeExamBrowser.Server.Contracts.Events.Proctoring;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring
|
||||
|
@ -21,7 +22,8 @@ namespace SafeExamBrowser.Proctoring
|
|||
public string Tooltip { get; protected set; }
|
||||
public IconResource IconResource { get; protected set; }
|
||||
|
||||
public abstract event NotificationChangedEventHandler NotificationChanged;
|
||||
internal event RemainingWorkUpdatedEventHandler RemainingWorkUpdated;
|
||||
public event NotificationChangedEventHandler NotificationChanged;
|
||||
|
||||
void INotification.Activate()
|
||||
{
|
||||
|
@ -40,7 +42,20 @@ namespace SafeExamBrowser.Proctoring
|
|||
internal abstract void Stop();
|
||||
internal abstract void Terminate();
|
||||
|
||||
internal virtual void ExecuteRemainingWork() { }
|
||||
internal virtual bool HasRemainingWork() => false;
|
||||
|
||||
protected virtual void ActivateNotification() { }
|
||||
protected virtual void TerminateNotification() { }
|
||||
|
||||
protected void InvokeNotificationChanged()
|
||||
{
|
||||
NotificationChanged?.Invoke();
|
||||
}
|
||||
|
||||
protected void InvokeRemainingWorkUpdated(RemainingWorkUpdatedEventArgs args)
|
||||
{
|
||||
RemainingWorkUpdated?.Invoke(args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -93,6 +93,7 @@
|
|||
<Compile Include="ProctoringFactory.cs" />
|
||||
<Compile Include="ProctoringImplementation.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="ScreenProctoring\Buffer.cs" />
|
||||
<Compile Include="ScreenProctoring\Cache.cs" />
|
||||
<Compile Include="ScreenProctoring\Data\IntervalTrigger.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.Collections.Generic;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
|
@ -27,20 +27,21 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
|
|||
|
||||
private readonly AppConfig appConfig;
|
||||
private readonly ILogger logger;
|
||||
private readonly Queue<(string fileName, int checksum, string hash)> queue;
|
||||
private readonly ConcurrentQueue<(string fileName, int checksum, string hash)> queue;
|
||||
|
||||
private string Directory { get; set; }
|
||||
internal int Count => queue.Count;
|
||||
internal string Directory { get; private set; }
|
||||
|
||||
public Cache(AppConfig appConfig, ILogger logger)
|
||||
{
|
||||
this.appConfig = appConfig;
|
||||
this.logger = logger;
|
||||
this.queue = new Queue<(string, int, string)>();
|
||||
this.queue = new ConcurrentQueue<(string, int, string)>();
|
||||
}
|
||||
|
||||
internal bool Any()
|
||||
{
|
||||
return false;
|
||||
return queue.Any();
|
||||
}
|
||||
|
||||
internal bool TryEnqueue(MetaData metaData, ScreenShot screenShot)
|
||||
|
@ -57,7 +58,7 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
|
|||
|
||||
success = true;
|
||||
|
||||
logger.Debug($"Cached data for '{fileName}'.");
|
||||
logger.Debug($"Cached data for '{fileName}', now holding {Count} item(s).");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
@ -74,23 +75,21 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
|
|||
metaData = default;
|
||||
screenShot = default;
|
||||
|
||||
if (queue.Any())
|
||||
if (queue.Any() && queue.TryPeek(out var item))
|
||||
{
|
||||
var (fileName, checksum, hash) = queue.Peek();
|
||||
|
||||
try
|
||||
{
|
||||
LoadData(fileName, out metaData, out screenShot);
|
||||
LoadImage(fileName, screenShot);
|
||||
Dequeue(fileName, checksum, hash, metaData, screenShot);
|
||||
LoadData(item.fileName, out metaData, out screenShot);
|
||||
LoadImage(item.fileName, screenShot);
|
||||
Dequeue(item.fileName, item.checksum, item.hash, metaData, screenShot);
|
||||
|
||||
success = true;
|
||||
|
||||
logger.Debug($"Uncached data for '{fileName}'.");
|
||||
logger.Debug($"Removed data for '{item.fileName}', {Count} item(s) remaining.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error($"Failed to uncache data for '{fileName}'!", e);
|
||||
logger.Error($"Failed to remove data for '{item.fileName}'!", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -116,7 +115,7 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
|
|||
File.Delete(dataPath);
|
||||
File.Delete(imagePath);
|
||||
|
||||
queue.Dequeue();
|
||||
while (!queue.TryDequeue(out _)) ;
|
||||
}
|
||||
|
||||
private void Enqueue(string fileName, MetaData metaData, ScreenShot screenShot)
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
using System;
|
||||
using SafeExamBrowser.Browser.Contracts;
|
||||
using SafeExamBrowser.Configuration.Contracts;
|
||||
using SafeExamBrowser.Core.Contracts.Notifications.Events;
|
||||
using SafeExamBrowser.Core.Contracts.Resources.Icons;
|
||||
using SafeExamBrowser.I18n.Contracts;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
|
@ -34,8 +33,6 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
|
|||
|
||||
internal override string Name => nameof(ScreenProctoring);
|
||||
|
||||
public override event NotificationChangedEventHandler NotificationChanged;
|
||||
|
||||
internal ScreenProctoringImplementation(
|
||||
AppConfig appConfig,
|
||||
IApplicationMonitor applicationMonitor,
|
||||
|
@ -54,6 +51,29 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
|
|||
this.text = text;
|
||||
}
|
||||
|
||||
internal override void ExecuteRemainingWork()
|
||||
{
|
||||
logger.Info("Starting execution of remaining work...");
|
||||
spooler.ExecuteRemainingWork(InvokeRemainingWorkUpdated);
|
||||
logger.Info("Terminated execution of remaining work.");
|
||||
}
|
||||
|
||||
internal override bool HasRemainingWork()
|
||||
{
|
||||
var hasWork = spooler.HasRemainingWork();
|
||||
|
||||
if (hasWork)
|
||||
{
|
||||
logger.Info("There is remaining work to be done.");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Info("There is no remaining work to be done.");
|
||||
}
|
||||
|
||||
return hasWork;
|
||||
}
|
||||
|
||||
internal override void Initialize()
|
||||
{
|
||||
var start = true;
|
||||
|
@ -132,10 +152,7 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
|
|||
|
||||
internal override void Terminate()
|
||||
{
|
||||
// TODO: Cache transmission or user information!
|
||||
|
||||
Stop();
|
||||
TerminateNotification();
|
||||
|
||||
logger.Info("Terminated proctoring.");
|
||||
}
|
||||
|
@ -189,7 +206,7 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
|
|||
Tooltip = text.Get(TextKey.Notification_ProctoringInactiveTooltip);
|
||||
}
|
||||
|
||||
NotificationChanged?.Invoke();
|
||||
InvokeNotificationChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,12 +8,11 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Timers;
|
||||
using SafeExamBrowser.Configuration.Contracts;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
using SafeExamBrowser.Proctoring.Contracts.Events;
|
||||
using SafeExamBrowser.Proctoring.ScreenProctoring.Data;
|
||||
using SafeExamBrowser.Proctoring.ScreenProctoring.Imaging;
|
||||
using SafeExamBrowser.Proctoring.ScreenProctoring.Service;
|
||||
|
@ -23,9 +22,10 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
|
|||
{
|
||||
internal class TransmissionSpooler
|
||||
{
|
||||
const int BAD = 10;
|
||||
const int GOOD = 0;
|
||||
private const int BAD = 10;
|
||||
private const int GOOD = 0;
|
||||
|
||||
private readonly Buffer buffer;
|
||||
private readonly Cache cache;
|
||||
private readonly ILogger logger;
|
||||
private readonly ConcurrentQueue<(MetaData metaData, ScreenShot screenShot)> queue;
|
||||
|
@ -33,8 +33,8 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
|
|||
private readonly ServiceProxy service;
|
||||
private readonly Timer timer;
|
||||
|
||||
private Queue<(MetaData metaData, DateTime schedule, ScreenShot screenShot)> buffer;
|
||||
private int health;
|
||||
private bool networkIssue;
|
||||
private bool recovering;
|
||||
private DateTime resume;
|
||||
private Thread thread;
|
||||
|
@ -42,7 +42,7 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
|
|||
|
||||
internal TransmissionSpooler(AppConfig appConfig, IModuleLogger logger, ServiceProxy service)
|
||||
{
|
||||
this.buffer = new Queue<(MetaData, DateTime, ScreenShot)>();
|
||||
this.buffer = new Buffer(logger.CloneFor(nameof(Buffer)));
|
||||
this.cache = new Cache(appConfig, logger.CloneFor(nameof(Cache)));
|
||||
this.logger = logger;
|
||||
this.queue = new ConcurrentQueue<(MetaData, ScreenShot)>();
|
||||
|
@ -56,6 +56,55 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
|
|||
queue.Enqueue((metaData, screenShot));
|
||||
}
|
||||
|
||||
internal void ExecuteRemainingWork(Action<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()
|
||||
{
|
||||
const int FIFTEEN_SECONDS = 15000;
|
||||
|
@ -167,14 +216,14 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
|
|||
{
|
||||
BufferFromCache();
|
||||
BufferFromQueue();
|
||||
TryTransmitFromBuffer();
|
||||
TransmitFromBuffer();
|
||||
}
|
||||
|
||||
private void ExecuteNormally()
|
||||
{
|
||||
TryTransmitFromBuffer();
|
||||
TryTransmitFromCache();
|
||||
TryTransmitFromQueue();
|
||||
TransmitFromBuffer();
|
||||
TransmitFromCache();
|
||||
TransmitFromQueue();
|
||||
}
|
||||
|
||||
private void ExecuteRecovery()
|
||||
|
@ -194,17 +243,11 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
|
|||
}
|
||||
}
|
||||
|
||||
private void Buffer(MetaData metaData, DateTime schedule, ScreenShot screenShot)
|
||||
{
|
||||
buffer.Enqueue((metaData, schedule, screenShot));
|
||||
buffer = new Queue<(MetaData, DateTime, ScreenShot)>(buffer.OrderBy((b) => b.schedule));
|
||||
}
|
||||
|
||||
private void BufferFromCache()
|
||||
{
|
||||
if (cache.TryDequeue(out var metaData, out var screenShot))
|
||||
{
|
||||
Buffer(metaData, CalculateSchedule(metaData), screenShot);
|
||||
buffer.Enqueue(metaData, CalculateSchedule(metaData), screenShot);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -212,37 +255,30 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
|
|||
{
|
||||
if (TryDequeue(out var metaData, out var screenShot))
|
||||
{
|
||||
Buffer(metaData, CalculateSchedule(metaData), screenShot);
|
||||
buffer.Enqueue(metaData, CalculateSchedule(metaData), screenShot);
|
||||
}
|
||||
}
|
||||
|
||||
private void CacheFromBuffer()
|
||||
{
|
||||
if (TryPeekFromBuffer(out var metaData, out _, out var screenShot))
|
||||
{
|
||||
var success = cache.TryEnqueue(metaData, screenShot);
|
||||
|
||||
if (success)
|
||||
if (buffer.TryPeek(out var metaData, out _, out var screenShot) && cache.TryEnqueue(metaData, screenShot))
|
||||
{
|
||||
buffer.Dequeue();
|
||||
screenShot.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CacheFromQueue()
|
||||
{
|
||||
if (TryDequeue(out var metaData, out var screenShot))
|
||||
{
|
||||
var success = cache.TryEnqueue(metaData, screenShot);
|
||||
|
||||
if (success)
|
||||
if (cache.TryEnqueue(metaData, screenShot))
|
||||
{
|
||||
screenShot.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
Buffer(metaData, DateTime.Now, screenShot);
|
||||
buffer.Enqueue(metaData, DateTime.Now, screenShot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -255,6 +291,48 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
|
|||
return schedule;
|
||||
}
|
||||
|
||||
private void TransmitFromBuffer()
|
||||
{
|
||||
var hasItem = buffer.TryPeek(out var metaData, out var schedule, out var screenShot);
|
||||
var ready = schedule <= DateTime.Now;
|
||||
|
||||
if (hasItem && ready && TryTransmit(metaData, screenShot))
|
||||
{
|
||||
buffer.Dequeue();
|
||||
screenShot.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void TransmitFromCache()
|
||||
{
|
||||
if (cache.TryDequeue(out var metaData, out var screenShot))
|
||||
{
|
||||
if (TryTransmit(metaData, screenShot))
|
||||
{
|
||||
screenShot.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer.Enqueue(metaData, DateTime.Now, screenShot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void TransmitFromQueue()
|
||||
{
|
||||
if (TryDequeue(out var metaData, out var screenShot))
|
||||
{
|
||||
if (TryTransmit(metaData, screenShot))
|
||||
{
|
||||
screenShot.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer.Enqueue(metaData, DateTime.Now, screenShot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryDequeue(out MetaData metaData, out ScreenShot screenShot)
|
||||
{
|
||||
metaData = default;
|
||||
|
@ -269,80 +347,6 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
|
|||
return metaData != default && screenShot != default;
|
||||
}
|
||||
|
||||
private bool TryPeekFromBuffer(out MetaData metaData, out DateTime schedule, out ScreenShot screenShot)
|
||||
{
|
||||
metaData = default;
|
||||
schedule = default;
|
||||
screenShot = default;
|
||||
|
||||
if (buffer.Any())
|
||||
{
|
||||
(metaData, schedule, screenShot) = buffer.Peek();
|
||||
}
|
||||
|
||||
return metaData != default && screenShot != default;
|
||||
}
|
||||
|
||||
private bool TryTransmitFromBuffer()
|
||||
{
|
||||
var success = false;
|
||||
|
||||
if (TryPeekFromBuffer(out var metaData, out var schedule, out var screenShot) && schedule <= DateTime.Now)
|
||||
{
|
||||
success = TryTransmit(metaData, screenShot);
|
||||
|
||||
if (success)
|
||||
{
|
||||
buffer.Dequeue();
|
||||
screenShot.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
private bool TryTransmitFromCache()
|
||||
{
|
||||
var success = true;
|
||||
|
||||
if (cache.TryDequeue(out var metaData, out var screenShot))
|
||||
{
|
||||
success = TryTransmit(metaData, screenShot);
|
||||
|
||||
if (success)
|
||||
{
|
||||
screenShot.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
Buffer(metaData, DateTime.Now, screenShot);
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
private bool TryTransmitFromQueue()
|
||||
{
|
||||
var success = false;
|
||||
|
||||
if (TryDequeue(out var metaData, out var screenShot))
|
||||
{
|
||||
success = TryTransmit(metaData, screenShot);
|
||||
|
||||
if (success)
|
||||
{
|
||||
screenShot.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
Buffer(metaData, DateTime.Now, screenShot);
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
private bool TryTransmit(MetaData metaData, ScreenShot screenShot)
|
||||
{
|
||||
var success = false;
|
||||
|
@ -351,10 +355,12 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
|
|||
{
|
||||
var response = service.Send(metaData, screenShot);
|
||||
|
||||
networkIssue = !response.Success;
|
||||
success = response.Success;
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
health = UpdateHealth(response.Value);
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
@ -371,6 +377,8 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
|
|||
{
|
||||
var response = service.GetHealth();
|
||||
|
||||
networkIssue = !response.Success;
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
health = UpdateHealth(response.Value);
|
||||
|
|
|
@ -103,6 +103,11 @@ namespace SafeExamBrowser.UserInterface.Contracts
|
|||
/// </summary>
|
||||
ISystemControl CreatePowerSupplyControl(IPowerSupply powerSupply, Location location);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new dialog to display the status of the proctoring finalization.
|
||||
/// </summary>
|
||||
IProctoringFinalizationDialog CreateProctoringFinalizationDialog();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new proctoring window loaded with the given proctoring control.
|
||||
/// </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\IPasswordDialog.cs" />
|
||||
<Compile Include="Windows\Data\PasswordDialogResult.cs" />
|
||||
<Compile Include="Proctoring\IProctoringFinalizationDialog.cs" />
|
||||
<Compile Include="Windows\IRuntimeWindow.cs" />
|
||||
<Compile Include="Windows\IServerFailureDialog.cs" />
|
||||
<Compile Include="Windows\ISplashScreen.cs" />
|
||||
|
|
|
@ -171,6 +171,9 @@
|
|||
<Compile Include="Windows\PasswordDialog.xaml.cs">
|
||||
<DependentUpon>PasswordDialog.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Windows\ProctoringFinalizationDialog.xaml.cs">
|
||||
<DependentUpon>ProctoringFinalizationDialog.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Windows\ProctoringWindow.xaml.cs">
|
||||
<DependentUpon>ProctoringWindow.xaml</DependentUpon>
|
||||
</Compile>
|
||||
|
@ -391,6 +394,10 @@
|
|||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Windows\ProctoringFinalizationDialog.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Windows\ProctoringWindow.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<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)
|
||||
{
|
||||
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">
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Windows\ProctoringFinalizationDialog.xaml.cs">
|
||||
<DependentUpon>ProctoringFinalizationDialog.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Windows\ProctoringWindow.xaml.cs">
|
||||
<DependentUpon>ProctoringWindow.xaml</DependentUpon>
|
||||
</Compile>
|
||||
|
@ -522,6 +525,10 @@
|
|||
<Generator>MSBuild:Compile</Generator>
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
<Page Include="Windows\ProctoringFinalizationDialog.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Windows\ProctoringWindow.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
<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)
|
||||
{
|
||||
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 uint MF_BYCOMMAND = 0x00000000;
|
||||
private const uint MF_GRAYED = 0x00000001;
|
||||
private const uint MF_ENABLED = 0x00000000;
|
||||
private const uint MF_GRAYED = 0x00000001;
|
||||
private const uint SC_CLOSE = 0xF060;
|
||||
private const uint SWP_SHOWWINDOW = 0x0040;
|
||||
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)
|
||||
{
|
||||
var helper = new WindowInteropHelper(window);
|
||||
|
|
Loading…
Reference in a new issue