seb-win-refactoring/SafeExamBrowser.Proctoring/ScreenProctoring/ScreenProctoringImplementation.cs

278 lines
7.2 KiB
C#

/*
* 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.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;
using SafeExamBrowser.Logging.Contracts;
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,
INativeMethods nativeMethods,
ServiceProxy service,
ProctoringSettings settings,
IText text)
{
this.logger = logger;
this.nativeMethods = nativeMethods;
this.service = service;
this.settings = settings.ScreenProctoring;
this.text = text;
this.timer = new Timer();
}
internal override void Initialize()
{
var start = true;
start &= !string.IsNullOrWhiteSpace(settings.ClientId);
start &= !string.IsNullOrWhiteSpace(settings.ClientSecret);
start &= !string.IsNullOrWhiteSpace(settings.GroupId);
start &= !string.IsNullOrWhiteSpace(settings.ServiceUrl);
timer.AutoReset = false;
timer.Interval = settings.MaxInterval;
if (start)
{
logger.Info($"Initialized proctoring: All settings are valid, starting automatically...");
Connect();
Start();
}
else
{
ShowNotificationInactive();
logger.Info($"Initialized proctoring: Not all settings are valid or a server session is active, not starting automatically.");
}
}
internal override void ProctoringConfigurationReceived(bool allowChat, bool receiveAudio, bool receiveVideo)
{
// Nothing to do here for now...
}
internal override void ProctoringInstructionReceived(InstructionEventArgs args)
{
if (args is ScreenProctoringInstruction instruction)
{
logger.Info($"Proctoring instruction received: {instruction.Method}.");
if (instruction.Method == InstructionMethod.Join)
{
settings.ClientId = instruction.ClientId;
settings.ClientSecret = instruction.ClientSecret;
settings.GroupId = instruction.GroupId;
settings.ServiceUrl = instruction.ServiceUrl;
Connect(instruction.SessionId);
Start();
}
else
{
Stop();
}
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();
ShowNotificationActive();
logger.Info($"Started proctoring.");
}
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();
TerminateServiceSession();
ShowNotificationInactive();
logger.Info("Stopped proctoring.");
}
internal override void Terminate()
{
Stop();
TerminateNotification();
logger.Info("Terminated proctoring.");
}
protected override void ActivateNotification()
{
// Nothing to do here for now...
}
protected override void TerminateNotification()
{
// Nothing to do here for now...
}
private void Connect(string sessionId = default)
{
logger.Info("Connecting to service...");
var connect = service.Connect(settings.ServiceUrl);
if (connect.Success)
{
if (sessionId == default)
{
logger.Info("Creating session...");
service.CreateSession(settings.GroupId);
}
else
{
service.SessionId = sessionId;
}
}
}
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();
}
private void ShowNotificationInactive()
{
// TODO: Replace with actual icon!
IconResource = new XamlIconResource { Uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/ProctoringNotification_Inactive.xaml") };
Tooltip = text.Get(TextKey.Notification_ProctoringInactiveTooltip);
NotificationChanged?.Invoke();
}
private void TerminateServiceSession()
{
if (service.IsConnected)
{
logger.Info("Terminating session...");
service.TerminateSession();
}
}
private void Timer_Elapsed(object sender, ElapsedEventArgs args)
{
TryExecute();
}
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);
}
}
}