SEBSP-107: Implemented screen proctoring finalization.

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

View file

@ -13,6 +13,7 @@ using SafeExamBrowser.Browser.Contracts;
using SafeExamBrowser.Communication.Contracts.Hosts;
using SafeExamBrowser.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>

View file

@ -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)

View file

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

View file

@ -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,

View file

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

View file

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

View file

@ -0,0 +1,58 @@
/*
* Copyright (c) 2023 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
namespace SafeExamBrowser.Proctoring.Contracts.Events
{
/// <summary>
/// The event arguments used for the remaining work updated event fired by the <see cref="IProctoringController"/>.
/// </summary>
public class RemainingWorkUpdatedEventArgs
{
/// <summary>
/// The path of the local cache, if <see cref="HasFailed"/> is <c>true</c>.
/// </summary>
public string CachePath { get; set; }
/// <summary>
/// Indicates that the execution of the remaining work has failed.
/// </summary>
public bool HasFailed { get; set; }
/// <summary>
/// Indicates that the execution of the remaining work has finished.
/// </summary>
public bool IsFinished { get; set; }
/// <summary>
/// Indicates that the execution is paused resp. waiting to be resumed.
/// </summary>
public bool IsWaiting { get; set; }
/// <summary>
/// The point in time when the next transmission will take place, if available.
/// </summary>
public DateTime? Next { get; set; }
/// <summary>
/// The number of already executed work items.
/// </summary>
public int Progress { get; set; }
/// <summary>
/// The point in time when the execution will resume, if <see cref="IsWaiting"/> is <c>true</c>.
/// </summary>
public DateTime Resume { get; set; }
/// <summary>
/// The total number of work items to be executed.
/// </summary>
public int Total { get; set; }
}
}

View file

@ -0,0 +1,15 @@
/*
* Copyright (c) 2023 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
namespace SafeExamBrowser.Proctoring.Contracts.Events
{
/// <summary>
/// Event handler used to indicate that the remaining work status has been updated.
/// </summary>
public delegate void RemainingWorkUpdatedEventHandler(RemainingWorkUpdatedEventArgs args);
}

View file

@ -38,6 +38,21 @@ namespace SafeExamBrowser.Proctoring.Contracts
/// </summary>
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();
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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" />

View file

@ -0,0 +1,92 @@
/*
* Copyright (c) 2023 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Proctoring.ScreenProctoring.Data;
using SafeExamBrowser.Proctoring.ScreenProctoring.Imaging;
namespace SafeExamBrowser.Proctoring.ScreenProctoring
{
internal class Buffer
{
private readonly object @lock = new object();
private readonly List<(MetaData metaData, DateTime schedule, ScreenShot screenShot)> list;
private readonly ILogger logger;
internal int Count
{
get
{
lock (@lock)
{
return list.Count;
}
}
}
internal Buffer(ILogger logger)
{
this.list = new List<(MetaData, DateTime, ScreenShot)>();
this.logger = logger;
}
internal bool Any()
{
lock (@lock)
{
return list.Any();
}
}
internal void Dequeue()
{
lock (@lock)
{
if (list.Any())
{
var (_, schedule, screenShot) = list.First();
list.RemoveAt(0);
logger.Debug($"Removed data for '{screenShot.CaptureTime:HH:mm:ss} -> {schedule:HH:mm:ss}', {Count} item(s) remaining.");
}
}
}
internal void Enqueue(MetaData metaData, DateTime schedule, ScreenShot screenShot)
{
lock (@lock)
{
list.Add((metaData, schedule, screenShot));
list.Sort((a, b) => DateTime.Compare(a.schedule, b.schedule));
logger.Debug($"Buffered data for '{screenShot.CaptureTime:HH:mm:ss} -> {schedule:HH:mm:ss}', now holding {Count} item(s).");
}
}
internal bool TryPeek(out MetaData metaData, out DateTime schedule, out ScreenShot screenShot)
{
lock (@lock)
{
metaData = default;
schedule = default;
screenShot = default;
if (list.Any())
{
(metaData, schedule, screenShot) = list.First();
}
return metaData != default && screenShot != default;
}
}
}
}

View file

@ -7,7 +7,7 @@
*/
using System;
using System.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)

View file

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

View file

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

View file

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

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2023 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using SafeExamBrowser.Proctoring.Contracts.Events;
namespace SafeExamBrowser.UserInterface.Contracts.Proctoring
{
/// <summary>
/// The dialog to display the status of the proctoring finalization.
/// </summary>
public interface IProctoringFinalizationDialog
{
/// <summary>
/// Shows the dialog as topmost window.
/// </summary>
void Show();
/// <summary>
/// Updates the status of the finalization.
/// </summary>
void Update(RemainingWorkUpdatedEventArgs status);
}
}

View file

@ -103,6 +103,7 @@
<Compile Include="Windows\ILockScreen.cs" />
<Compile Include="Windows\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" />

View file

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

View file

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

View file

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

View file

@ -0,0 +1,114 @@
/*
* Copyright (c) 2023 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System.Windows;
using System.Windows.Input;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Proctoring.Contracts.Events;
using SafeExamBrowser.UserInterface.Contracts.Proctoring;
using SafeExamBrowser.UserInterface.Shared.Utilities;
namespace SafeExamBrowser.UserInterface.Desktop.Windows
{
public partial class ProctoringFinalizationDialog : Window, IProctoringFinalizationDialog
{
private readonly IText text;
public ProctoringFinalizationDialog(IText text)
{
this.text = text;
InitializeComponent();
InitializeDialog();
}
public new void Show()
{
Dispatcher.Invoke(() => ShowDialog());
}
public void Update(RemainingWorkUpdatedEventArgs status)
{
Dispatcher.Invoke(() =>
{
if (status.HasFailed)
{
ShowFailure(status);
}
else if (status.IsFinished)
{
Close();
}
else
{
ShowProgress(status);
}
});
}
private void InitializeDialog()
{
Button.Click += (o, args) => Close();
Button.Content = text.Get(TextKey.ProctoringFinalizationDialog_Confirm);
Loaded += (o, args) => this.DisableCloseButton();
Title = text.Get(TextKey.ProctoringFinalizationDialog_Title);
}
private void ShowFailure(RemainingWorkUpdatedEventArgs status)
{
ButtonPanel.Visibility = Visibility.Visible;
CachePath.Text = status.CachePath;
Cursor = Cursors.Arrow;
FailurePanel.Visibility = Visibility.Visible;
Message.Text = text.Get(TextKey.ProctoringFinalizationDialog_FailureMessage);
ProgressPanel.Visibility = Visibility.Collapsed;
this.EnableCloseButton();
}
private void ShowProgress(RemainingWorkUpdatedEventArgs status)
{
ButtonPanel.Visibility = Visibility.Collapsed;
Cursor = Cursors.Wait;
FailurePanel.Visibility = Visibility.Collapsed;
Info.Text = text.Get(TextKey.ProctoringFinalizationDialog_InfoMessage);
ProgressPanel.Visibility = Visibility.Visible;
if (status.IsWaiting)
{
var count = $"{status.Total - status.Progress}";
var time = $"{status.Resume.ToLongTimeString()}";
Percentage.Text = "";
Progress.IsIndeterminate = true;
Status.Text = text.Get(TextKey.ProctoringFinalizationDialog_StatusWaiting).Replace("%%_COUNT_%%", count).Replace("%%_TIME_%%", time);
}
else
{
var count = $"{status.Progress}";
var total = $"{status.Total}";
Percentage.Text = $"{status.Progress / (double) (status.Total > 0 ? status.Total : 1) * 100:N0}%";
Progress.IsIndeterminate = false;
Progress.Maximum = status.Total;
Progress.Value = status.Progress;
if (status.Next.HasValue)
{
Status.Text = text.Get(TextKey.ProctoringFinalizationDialog_StatusAndTime).Replace("%%_TIME_%%", $"{status.Next.Value.ToLongTimeString()}");
}
else
{
Status.Text = text.Get(TextKey.ProctoringFinalizationDialog_Status);
}
Status.Text = Status.Text.Replace("%%_COUNT_%%", count).Replace("%%_TOTAL_%%", total);
}
}
}
}

View file

@ -179,6 +179,9 @@
<Compile Include="Properties\AssemblyInfo.cs">
<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>

View file

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

View file

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

View file

@ -0,0 +1,135 @@
/*
* Copyright (c) 2023 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Proctoring.Contracts.Events;
using SafeExamBrowser.UserInterface.Contracts.Proctoring;
namespace SafeExamBrowser.UserInterface.Mobile.Windows
{
public partial class ProctoringFinalizationDialog : Window, IProctoringFinalizationDialog
{
private readonly IText text;
public ProctoringFinalizationDialog(IText text)
{
this.text = text;
InitializeComponent();
InitializeDialog();
}
public new void Show()
{
Dispatcher.Invoke(() =>
{
InitializeBounds();
ShowDialog();
});
}
public void Update(RemainingWorkUpdatedEventArgs status)
{
Dispatcher.Invoke(() =>
{
if (status.HasFailed)
{
ShowFailure(status);
}
else if (status.IsFinished)
{
Close();
}
else
{
ShowProgress(status);
}
});
}
private void InitializeBounds()
{
Left = 0;
Top = 0;
Height = SystemParameters.PrimaryScreenHeight;
Width = SystemParameters.PrimaryScreenWidth;
}
private void InitializeDialog()
{
Button.Click += (o, args) => Close();
Button.Content = text.Get(TextKey.ProctoringFinalizationDialog_Confirm);
Title = text.Get(TextKey.ProctoringFinalizationDialog_Title);
InitializeBounds();
SystemParameters.StaticPropertyChanged += SystemParameters_StaticPropertyChanged;
}
private void ShowFailure(RemainingWorkUpdatedEventArgs status)
{
ButtonPanel.Visibility = Visibility.Visible;
CachePath.Text = status.CachePath;
Cursor = Cursors.Arrow;
FailurePanel.Visibility = Visibility.Visible;
Message.Text = text.Get(TextKey.ProctoringFinalizationDialog_FailureMessage);
ProgressPanel.Visibility = Visibility.Collapsed;
}
private void ShowProgress(RemainingWorkUpdatedEventArgs status)
{
ButtonPanel.Visibility = Visibility.Collapsed;
Cursor = Cursors.Wait;
FailurePanel.Visibility = Visibility.Collapsed;
Info.Text = text.Get(TextKey.ProctoringFinalizationDialog_InfoMessage);
ProgressPanel.Visibility = Visibility.Visible;
if (status.IsWaiting)
{
var count = $"{status.Total - status.Progress}";
var time = $"{status.Resume.ToLongTimeString()}";
Percentage.Text = "";
Progress.IsIndeterminate = true;
Status.Text = text.Get(TextKey.ProctoringFinalizationDialog_StatusWaiting).Replace("%%_COUNT_%%", count).Replace("%%_TIME_%%", time);
}
else
{
var count = $"{status.Progress}";
var total = $"{status.Total}";
Percentage.Text = $"{status.Progress / (double) (status.Total > 0 ? status.Total : 1) * 100:N0}%";
Progress.IsIndeterminate = false;
Progress.Maximum = status.Total;
Progress.Value = status.Progress;
if (status.Next.HasValue)
{
Status.Text = text.Get(TextKey.ProctoringFinalizationDialog_StatusAndTime).Replace("%%_TIME_%%", $"{status.Next.Value.ToLongTimeString()}");
}
else
{
Status.Text = text.Get(TextKey.ProctoringFinalizationDialog_Status);
}
Status.Text = Status.Text.Replace("%%_COUNT_%%", count).Replace("%%_TOTAL_%%", total);
}
}
private void SystemParameters_StaticPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(SystemParameters.WorkArea))
{
Dispatcher.InvokeAsync(InitializeBounds);
}
}
}
}

View file

@ -17,8 +17,8 @@ namespace SafeExamBrowser.UserInterface.Shared.Utilities
{
private const int GWL_STYLE = -16;
private const 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);