SEBSP-26: Implemented capturing and transmission interval.

This commit is contained in:
Damian Büchel 2024-02-06 10:45:45 +01:00
parent 456894edb9
commit cb81906945
8 changed files with 118 additions and 32 deletions

View file

@ -288,7 +288,7 @@ namespace SafeExamBrowser.Client
private IOperation BuildProctoringOperation()
{
var controller = new ProctoringController(context.AppConfig, new FileSystem(), ModuleLogger(nameof(ProctoringController)), context.Server, text, uiFactory);
var controller = new ProctoringController(context.AppConfig, new FileSystem(), ModuleLogger(nameof(ProctoringController)), nativeMethods, context.Server, text, uiFactory);
var operation = new ProctoringOperation(actionCenter, context, controller, logger, taskbar, uiFactory);
return operation;

View file

@ -90,6 +90,9 @@ namespace SafeExamBrowser.Configuration.ConfigurationData.DataMapping
case Keys.Proctoring.ScreenProctoring.ClientSecret:
MapClientSecret(settings, value);
break;
case Keys.Proctoring.ScreenProctoring.Enabled:
MapScreenProctoringEnabled(settings, value);
break;
case Keys.Proctoring.ScreenProctoring.GroupId:
MapGroupId(settings, value);
break;
@ -108,9 +111,6 @@ namespace SafeExamBrowser.Configuration.ConfigurationData.DataMapping
case Keys.Proctoring.ScreenProctoring.MinInterval:
MapMinInterval(settings, value);
break;
case Keys.Proctoring.ScreenProctoring.Enabled:
MapScreenProctoringEnabled(settings, value);
break;
case Keys.Proctoring.ScreenProctoring.ServiceUrl:
MapServiceUrl(settings, value);
break;

View file

@ -19,6 +19,7 @@ using SafeExamBrowser.Server.Contracts.Events.Proctoring;
using SafeExamBrowser.Settings.Proctoring;
using SafeExamBrowser.SystemComponents.Contracts;
using SafeExamBrowser.UserInterface.Contracts;
using SafeExamBrowser.WindowsApi.Contracts;
namespace SafeExamBrowser.Proctoring
{
@ -40,6 +41,7 @@ namespace SafeExamBrowser.Proctoring
AppConfig appConfig,
IFileSystem fileSystem,
IModuleLogger logger,
INativeMethods nativeMethods,
IServerProxy server,
IText text,
IUserInterfaceFactory uiFactory)
@ -47,7 +49,7 @@ namespace SafeExamBrowser.Proctoring
this.logger = logger;
this.server = server;
factory = new ProctoringFactory(appConfig, fileSystem, logger, text, uiFactory);
factory = new ProctoringFactory(appConfig, fileSystem, logger, nativeMethods, text, uiFactory);
implementations = new List<ProctoringImplementation>();
}

View file

@ -16,6 +16,7 @@ using SafeExamBrowser.Proctoring.ScreenProctoring.Service;
using SafeExamBrowser.Settings.Proctoring;
using SafeExamBrowser.SystemComponents.Contracts;
using SafeExamBrowser.UserInterface.Contracts;
using SafeExamBrowser.WindowsApi.Contracts;
namespace SafeExamBrowser.Proctoring
{
@ -24,14 +25,22 @@ namespace SafeExamBrowser.Proctoring
private readonly AppConfig appConfig;
private readonly IFileSystem fileSystem;
private readonly IModuleLogger logger;
private readonly INativeMethods nativeMethods;
private readonly IText text;
private readonly IUserInterfaceFactory uiFactory;
public ProctoringFactory(AppConfig appConfig, IFileSystem fileSystem, IModuleLogger logger, IText text, IUserInterfaceFactory uiFactory)
public ProctoringFactory(
AppConfig appConfig,
IFileSystem fileSystem,
IModuleLogger logger,
INativeMethods nativeMethods,
IText text,
IUserInterfaceFactory uiFactory)
{
this.appConfig = appConfig;
this.fileSystem = fileSystem;
this.logger = logger;
this.nativeMethods = nativeMethods;
this.text = text;
this.uiFactory = uiFactory;
}
@ -52,7 +61,7 @@ namespace SafeExamBrowser.Proctoring
var logger = this.logger.CloneFor(nameof(ScreenProctoring));
var service = new ServiceProxy(logger.CloneFor(nameof(ServiceProxy)));
implementations.Add(new ScreenProctoringImplementation(logger, service, settings, text));
implementations.Add(new ScreenProctoringImplementation(logger, nativeMethods, service, settings, text));
}
return implementations;

View file

@ -145,6 +145,10 @@
<Project>{c7889e97-6ff6-4a58-b7cb-521ed276b316}</Project>
<Name>SafeExamBrowser.UserInterface.Contracts</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.WindowsApi.Contracts\SafeExamBrowser.WindowsApi.Contracts.csproj">
<Project>{7016F080-9AA5-41B2-A225-385AD877C171}</Project>
<Name>SafeExamBrowser.WindowsApi.Contracts</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="JitsiMeet\index.html" />

View file

@ -46,12 +46,12 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring.Imaging
public string ToReducedString()
{
return $"{Width}x{Height}, {Data.Length / 1000:N0} kB, {Format.ToString().ToUpper()}";
return $"{Width}x{Height}, {Data.Length / 1000:N0}kB, {Format.ToString().ToUpper()}";
}
public override string ToString()
{
return $"resolution: {Width}x{Height}, size: {Data.Length / 1000:N0} kB, format: {Format.ToString().ToUpper()}";
return $"resolution: {Width}x{Height}, size: {Data.Length / 1000:N0}kB, format: {Format.ToString().ToUpper()}";
}
internal void Compress()

View file

@ -7,7 +7,10 @@
*/
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
using System.Windows.Input;
using SafeExamBrowser.Core.Contracts.Notifications.Events;
using SafeExamBrowser.Core.Contracts.Resources.Icons;
using SafeExamBrowser.I18n.Contracts;
@ -16,24 +19,42 @@ using SafeExamBrowser.Proctoring.ScreenProctoring.Imaging;
using SafeExamBrowser.Proctoring.ScreenProctoring.Service;
using SafeExamBrowser.Server.Contracts.Events.Proctoring;
using SafeExamBrowser.Settings.Proctoring;
using SafeExamBrowser.WindowsApi.Contracts;
using SafeExamBrowser.WindowsApi.Contracts.Events;
using MouseButton = SafeExamBrowser.WindowsApi.Contracts.Events.MouseButton;
using MouseButtonState = SafeExamBrowser.WindowsApi.Contracts.Events.MouseButtonState;
using Timer = System.Timers.Timer;
namespace SafeExamBrowser.Proctoring.ScreenProctoring
{
internal class ScreenProctoringImplementation : ProctoringImplementation
{
private readonly object @lock = new object();
private readonly IModuleLogger logger;
private readonly INativeMethods nativeMethods;
private readonly ServiceProxy service;
private readonly ScreenProctoringSettings settings;
private readonly IText text;
private readonly Timer timer;
private DateTime last;
private Guid? keyboardHookId;
private Guid? mouseHookId;
internal override string Name => nameof(ScreenProctoring);
public override event NotificationChangedEventHandler NotificationChanged;
internal ScreenProctoringImplementation(IModuleLogger logger, ServiceProxy service, ProctoringSettings settings, IText text)
internal ScreenProctoringImplementation(
IModuleLogger logger,
INativeMethods nativeMethods,
ServiceProxy service,
ProctoringSettings settings,
IText text)
{
this.logger = logger;
this.nativeMethods = nativeMethods;
this.service = service;
this.settings = settings.ScreenProctoring;
this.text = text;
@ -62,7 +83,6 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
else
{
ShowNotificationInactive();
logger.Info($"Initialized proctoring: Not all settings are valid or a server session is active, not starting automatically.");
}
}
@ -96,9 +116,12 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
logger.Info("Successfully processed instruction.");
}
}
internal override void Start()
{
last = DateTime.Now;
keyboardHookId = nativeMethods.RegisterKeyboardHook(KeyboardHookCallback);
mouseHookId = nativeMethods.RegisterMouseHook(MouseHookCallback);
timer.Elapsed += Timer_Elapsed;
timer.Start();
@ -109,6 +132,19 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
internal override void Stop()
{
if (keyboardHookId.HasValue)
{
nativeMethods.DeregisterKeyboardHook(keyboardHookId.Value);
}
if (mouseHookId.HasValue)
{
nativeMethods.DeregisterMouseHook(mouseHookId.Value);
}
keyboardHookId = default;
mouseHookId = default;
timer.Elapsed -= Timer_Elapsed;
timer.Stop();
@ -120,11 +156,7 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
internal override void Terminate()
{
if (timer.Enabled)
{
Stop();
}
Stop();
TerminateNotification();
logger.Info("Terminated proctoring.");
@ -160,9 +192,28 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
}
}
private bool KeyboardHookCallback(int keyCode, KeyModifier modifier, KeyState state)
{
var key = KeyInterop.KeyFromVirtualKey(keyCode);
TryExecute();
return false;
}
private bool MouseHookCallback(MouseButton button, MouseButtonState state, MouseInformation info)
{
var isTouch = info.IsTouch;
TryExecute();
return false;
}
private void ShowNotificationActive()
{
// TODO: Replace with actual icon!
// TODO: Extend INotification with IsEnabled or CanActivate, as the screen proctoring notification does not have any action or window!
IconResource = new XamlIconResource { Uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/ProctoringNotification_Active.xaml") };
Tooltip = text.Get(TextKey.Notification_ProctoringActiveTooltip);
NotificationChanged?.Invoke();
@ -187,21 +238,41 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
private void Timer_Elapsed(object sender, ElapsedEventArgs args)
{
try
{
using (var screenShot = new ScreenShot(logger.CloneFor(nameof(ScreenShot)), settings))
{
screenShot.Take();
screenShot.Compress();
service.SendScreenShot(screenShot);
}
}
catch (Exception e)
{
logger.Error("Failed to process screen shot!", e);
}
TryExecute();
}
timer.Start();
private void TryExecute()
{
if (MinimumIntervalElapsed() && Monitor.TryEnter(@lock))
{
last = DateTime.Now;
timer.Stop();
Task.Run(() =>
{
try
{
using (var screenShot = new ScreenShot(logger.CloneFor(nameof(ScreenShot)), settings))
{
screenShot.Take();
screenShot.Compress();
service.Send(screenShot);
}
}
catch (Exception e)
{
logger.Error("Failed to process screen shot!", e);
}
});
timer.Start();
Monitor.Exit(@lock);
}
}
private bool MinimumIntervalElapsed()
{
return DateTime.Now.Subtract(last) >= new TimeSpan(0, 0, 0, 0, settings.MinInterval);
}
}
}

View file

@ -69,7 +69,7 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring.Service
return new ServiceResponse(success, message);
}
internal ServiceResponse SendScreenShot(ScreenShot screenShot)
internal ServiceResponse Send(ScreenShot screenShot)
{
var request = new ScreenShotRequest(api, httpClient, logger, parser);
var success = request.TryExecute(screenShot, SessionId, out var message);