diff --git a/SafeExamBrowser.Client/CompositionRoot.cs b/SafeExamBrowser.Client/CompositionRoot.cs index a0fdac3a..8d303439 100644 --- a/SafeExamBrowser.Client/CompositionRoot.cs +++ b/SafeExamBrowser.Client/CompositionRoot.cs @@ -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; diff --git a/SafeExamBrowser.Configuration/ConfigurationData/DataMapping/ProctoringDataMapper.cs b/SafeExamBrowser.Configuration/ConfigurationData/DataMapping/ProctoringDataMapper.cs index d0c3d94d..5441a513 100644 --- a/SafeExamBrowser.Configuration/ConfigurationData/DataMapping/ProctoringDataMapper.cs +++ b/SafeExamBrowser.Configuration/ConfigurationData/DataMapping/ProctoringDataMapper.cs @@ -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; diff --git a/SafeExamBrowser.Proctoring/ProctoringController.cs b/SafeExamBrowser.Proctoring/ProctoringController.cs index 5de2cf2e..8d598e88 100644 --- a/SafeExamBrowser.Proctoring/ProctoringController.cs +++ b/SafeExamBrowser.Proctoring/ProctoringController.cs @@ -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(); } diff --git a/SafeExamBrowser.Proctoring/ProctoringFactory.cs b/SafeExamBrowser.Proctoring/ProctoringFactory.cs index 15a71f1f..9b0b7f86 100644 --- a/SafeExamBrowser.Proctoring/ProctoringFactory.cs +++ b/SafeExamBrowser.Proctoring/ProctoringFactory.cs @@ -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; diff --git a/SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj b/SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj index d9b62dce..057d5f7b 100644 --- a/SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj +++ b/SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj @@ -145,6 +145,10 @@ {c7889e97-6ff6-4a58-b7cb-521ed276b316} SafeExamBrowser.UserInterface.Contracts + + {7016F080-9AA5-41B2-A225-385AD877C171} + SafeExamBrowser.WindowsApi.Contracts + diff --git a/SafeExamBrowser.Proctoring/ScreenProctoring/Imaging/ScreenShot.cs b/SafeExamBrowser.Proctoring/ScreenProctoring/Imaging/ScreenShot.cs index ff75b856..561517db 100644 --- a/SafeExamBrowser.Proctoring/ScreenProctoring/Imaging/ScreenShot.cs +++ b/SafeExamBrowser.Proctoring/ScreenProctoring/Imaging/ScreenShot.cs @@ -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() diff --git a/SafeExamBrowser.Proctoring/ScreenProctoring/ScreenProctoringImplementation.cs b/SafeExamBrowser.Proctoring/ScreenProctoring/ScreenProctoringImplementation.cs index b58bf2be..47186416 100644 --- a/SafeExamBrowser.Proctoring/ScreenProctoring/ScreenProctoringImplementation.cs +++ b/SafeExamBrowser.Proctoring/ScreenProctoring/ScreenProctoringImplementation.cs @@ -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); } } } diff --git a/SafeExamBrowser.Proctoring/ScreenProctoring/Service/ServiceProxy.cs b/SafeExamBrowser.Proctoring/ScreenProctoring/Service/ServiceProxy.cs index 210b71bf..44d43e67 100644 --- a/SafeExamBrowser.Proctoring/ScreenProctoring/Service/ServiceProxy.cs +++ b/SafeExamBrowser.Proctoring/ScreenProctoring/Service/ServiceProxy.cs @@ -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);