From f902ee9598f0ec219289765d23b6b78a920fa502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damian=20B=C3=BCchel?= Date: Wed, 21 Feb 2024 18:37:23 +0100 Subject: [PATCH] SEBSP-23: Finished basic network redundancy. This build contains a service health simulation. --- .../ProctoringFactory.cs | 2 +- .../SafeExamBrowser.Proctoring.csproj | 7 +- .../ScreenProctoring/Cache.cs | 242 +++++++++++++++ .../ScreenProctoring/Data/IntervalTrigger.cs | 1 - .../Data/MetaDataAggregator.cs | 138 +++++++++ .../ScreenProctoring/Data/Metadata.cs | 120 +------- .../ScreenProctoring/DataCollector.cs | 11 +- .../Events/DataCollectedEventHandler.cs | 2 +- .../ScreenProctoring/Imaging/ScreenShot.cs | 132 +-------- .../Imaging/ScreenShotProcessor.cs | 160 ++++++++++ .../ScreenProctoringImplementation.cs | 8 +- .../ScreenProctoring/Service/Parser.cs | 11 +- .../Service/Requests/ScreenShotRequest.cs | 4 +- .../ScreenProctoring/Service/ServiceProxy.cs | 4 +- .../ScreenProctoring/TransmissionSpooler.cs | 277 ++++++++++-------- SafeExamBrowser.Server/Extensions.cs | 10 +- .../Requests/ExamConfigurationRequest.cs | 2 +- .../Requests/PingRequest.cs | 2 +- 18 files changed, 744 insertions(+), 389 deletions(-) create mode 100644 SafeExamBrowser.Proctoring/ScreenProctoring/Cache.cs create mode 100644 SafeExamBrowser.Proctoring/ScreenProctoring/Data/MetaDataAggregator.cs create mode 100644 SafeExamBrowser.Proctoring/ScreenProctoring/Imaging/ScreenShotProcessor.cs diff --git a/SafeExamBrowser.Proctoring/ProctoringFactory.cs b/SafeExamBrowser.Proctoring/ProctoringFactory.cs index d3129a4b..64139060 100644 --- a/SafeExamBrowser.Proctoring/ProctoringFactory.cs +++ b/SafeExamBrowser.Proctoring/ProctoringFactory.cs @@ -69,7 +69,7 @@ namespace SafeExamBrowser.Proctoring var logger = this.logger.CloneFor(nameof(ScreenProctoring)); var service = new ServiceProxy(logger.CloneFor(nameof(ServiceProxy))); - implementations.Add(new ScreenProctoringImplementation(applicationMonitor, browser, logger, nativeMethods, service, settings, text)); + implementations.Add(new ScreenProctoringImplementation(appConfig, applicationMonitor, browser, logger, nativeMethods, service, settings, text)); } return implementations; diff --git a/SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj b/SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj index 4ffa2b82..84f4b465 100644 --- a/SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj +++ b/SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj @@ -82,6 +82,8 @@ + + @@ -91,14 +93,17 @@ + + - + + diff --git a/SafeExamBrowser.Proctoring/ScreenProctoring/Cache.cs b/SafeExamBrowser.Proctoring/ScreenProctoring/Cache.cs new file mode 100644 index 00000000..70f2b316 --- /dev/null +++ b/SafeExamBrowser.Proctoring/ScreenProctoring/Cache.cs @@ -0,0 +1,242 @@ +/* + * 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.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Xml.Linq; +using SafeExamBrowser.Configuration.Contracts; +using SafeExamBrowser.Logging.Contracts; +using SafeExamBrowser.Proctoring.ScreenProctoring.Data; +using SafeExamBrowser.Proctoring.ScreenProctoring.Imaging; +using SafeExamBrowser.Settings.Proctoring; + +namespace SafeExamBrowser.Proctoring.ScreenProctoring +{ + internal class Cache + { + private const string DATA_FILE_EXTENSION = "xml"; + + private readonly AppConfig appConfig; + private readonly ILogger logger; + private readonly Queue<(string fileName, int checksum, string hash)> queue; + + private string Directory { get; set; } + + public Cache(AppConfig appConfig, ILogger logger) + { + this.appConfig = appConfig; + this.logger = logger; + this.queue = new Queue<(string, int, string)>(); + } + + internal bool Any() + { + return false; + } + + internal bool TryEnqueue(MetaData metaData, ScreenShot screenShot) + { + var fileName = $"{screenShot.CaptureTime:yyyy-MM-dd HH\\hmm\\mss\\sfff\\m\\s}"; + var success = false; + + try + { + InitializeDirectory(); + SaveData(fileName, metaData, screenShot); + SaveImage(fileName, screenShot); + Enqueue(fileName, metaData, screenShot); + + success = true; + + logger.Debug($"Cached data for '{fileName}'."); + } + catch (Exception e) + { + logger.Error($"Failed to cache data for '{fileName}'!", e); + } + + return success; + } + + internal bool TryDequeue(out MetaData metaData, out ScreenShot screenShot) + { + var success = false; + + metaData = default; + screenShot = default; + + if (queue.Any()) + { + var (fileName, checksum, hash) = queue.Peek(); + + try + { + LoadData(fileName, out metaData, out screenShot); + LoadImage(fileName, screenShot); + Dequeue(fileName, checksum, hash, metaData, screenShot); + + success = true; + + logger.Debug($"Uncached data for '{fileName}'."); + } + catch (Exception e) + { + logger.Error($"Failed to uncache data for '{fileName}'!", e); + } + } + + return success; + } + + private void Dequeue(string fileName, int checksum, string hash, MetaData metaData, ScreenShot screenShot) + { + var dataPath = Path.Combine(Directory, $"{fileName}.{DATA_FILE_EXTENSION}"); + var extension = screenShot.Format.ToString().ToLower(); + var imagePath = Path.Combine(Directory, $"{fileName}.{extension}"); + + if (checksum != GenerateChecksum(screenShot)) + { + logger.Warn($"The checksum for '{fileName}' does not match, the image data may be manipulated!"); + } + + if (hash != GenerateHash(metaData, screenShot)) + { + logger.Warn($"The hash for '{fileName}' does not match, the metadata may be manipulated!"); + } + + File.Delete(dataPath); + File.Delete(imagePath); + + queue.Dequeue(); + } + + private void Enqueue(string fileName, MetaData metaData, ScreenShot screenShot) + { + var checksum = GenerateChecksum(screenShot); + var hash = GenerateHash(metaData, screenShot); + + queue.Enqueue((fileName, checksum, hash)); + } + + private int GenerateChecksum(ScreenShot screenShot) + { + var checksum = default(int); + + foreach (var data in screenShot.Data) + { + unchecked + { + checksum += data; + } + } + + return checksum; + } + + private string GenerateHash(MetaData metaData, ScreenShot screenShot) + { + var hash = default(string); + + using (var algorithm = new SHA256Managed()) + { + var input = metaData.ToJson() + screenShot.CaptureTime + screenShot.Format + screenShot.Height + screenShot.Width; + var bytes = Encoding.UTF8.GetBytes(input); + var result = algorithm.ComputeHash(bytes); + + hash = string.Join(string.Empty, result.Select(b => $"{b:x2}")); + } + + return hash; + } + + private void InitializeDirectory() + { + if (Directory == default) + { + Directory = Path.Combine(appConfig.TemporaryDirectory, nameof(ScreenProctoring)); + } + + if (!System.IO.Directory.Exists(Directory)) + { + System.IO.Directory.CreateDirectory(Directory); + logger.Debug($"Created caching directory '{Directory}'."); + } + } + + private void LoadData(string fileName, out MetaData metaData, out ScreenShot screenShot) + { + var dataPath = Path.Combine(Directory, $"{fileName}.{DATA_FILE_EXTENSION}"); + var document = XDocument.Load(dataPath); + var xml = document.Descendants(nameof(MetaData)).First(); + + metaData = new MetaData(); + screenShot = new ScreenShot(); + + metaData.ApplicationInfo = xml.Descendants(nameof(MetaData.ApplicationInfo)).First().Value; + metaData.BrowserInfo = xml.Descendants(nameof(MetaData.BrowserInfo)).First().Value; + metaData.Elapsed = TimeSpan.Parse(xml.Descendants(nameof(MetaData.Elapsed)).First().Value); + metaData.TriggerInfo = xml.Descendants(nameof(MetaData.TriggerInfo)).First().Value; + metaData.Urls = xml.Descendants(nameof(MetaData.Urls)).First().Value; + metaData.WindowTitle = xml.Descendants(nameof(MetaData.WindowTitle)).First().Value; + + xml = document.Descendants(nameof(ScreenShot)).First(); + + screenShot.CaptureTime = DateTime.Parse(xml.Descendants(nameof(ScreenShot.CaptureTime)).First().Value); + screenShot.Format = (ImageFormat) Enum.Parse(typeof(ImageFormat), xml.Descendants(nameof(ScreenShot.Format)).First().Value); + screenShot.Height = int.Parse(xml.Descendants(nameof(ScreenShot.Height)).First().Value); + screenShot.Width = int.Parse(xml.Descendants(nameof(ScreenShot.Width)).First().Value); + } + + private void LoadImage(string fileName, ScreenShot screenShot) + { + var extension = screenShot.Format.ToString().ToLower(); + var imagePath = Path.Combine(Directory, $"{fileName}.{extension}"); + + screenShot.Data = File.ReadAllBytes(imagePath); + } + + private void SaveData(string fileName, MetaData metaData, ScreenShot screenShot) + { + var data = new XDocument(new XDeclaration("1.0", "utf-8", "yes")); + var dataPath = Path.Combine(Directory, $"{fileName}.{DATA_FILE_EXTENSION}"); + + data.Add( + new XElement("Data", + new XElement(nameof(MetaData), + new XElement(nameof(MetaData.ApplicationInfo), metaData.ApplicationInfo), + new XElement(nameof(MetaData.BrowserInfo), metaData.BrowserInfo), + new XElement(nameof(MetaData.Elapsed), metaData.Elapsed.ToString()), + new XElement(nameof(MetaData.TriggerInfo), metaData.TriggerInfo), + new XElement(nameof(MetaData.Urls), metaData.Urls), + new XElement(nameof(MetaData.WindowTitle), metaData.WindowTitle) + ), + new XElement(nameof(ScreenShot), + new XElement(nameof(ScreenShot.CaptureTime), screenShot.CaptureTime.ToString()), + new XElement(nameof(ScreenShot.Format), screenShot.Format), + new XElement(nameof(ScreenShot.Height), screenShot.Height), + new XElement(nameof(ScreenShot.Width), screenShot.Width) + ) + ) + ); + + data.Save(dataPath); + } + + private void SaveImage(string fileName, ScreenShot screenShot) + { + var extension = screenShot.Format.ToString().ToLower(); + var imagePath = Path.Combine(Directory, $"{fileName}.{extension}"); + + File.WriteAllBytes(imagePath, screenShot.Data); + } + } +} diff --git a/SafeExamBrowser.Proctoring/ScreenProctoring/Data/IntervalTrigger.cs b/SafeExamBrowser.Proctoring/ScreenProctoring/Data/IntervalTrigger.cs index e351731e..a5a8291c 100644 --- a/SafeExamBrowser.Proctoring/ScreenProctoring/Data/IntervalTrigger.cs +++ b/SafeExamBrowser.Proctoring/ScreenProctoring/Data/IntervalTrigger.cs @@ -11,6 +11,5 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring.Data internal class IntervalTrigger { public int ConfigurationValue { get; internal set; } - public int TimeElapsed { get; internal set; } } } diff --git a/SafeExamBrowser.Proctoring/ScreenProctoring/Data/MetaDataAggregator.cs b/SafeExamBrowser.Proctoring/ScreenProctoring/Data/MetaDataAggregator.cs new file mode 100644 index 00000000..1db5e3d7 --- /dev/null +++ b/SafeExamBrowser.Proctoring/ScreenProctoring/Data/MetaDataAggregator.cs @@ -0,0 +1,138 @@ +/* + * 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.Linq; +using System.Text; +using SafeExamBrowser.Browser.Contracts; +using SafeExamBrowser.Logging.Contracts; +using SafeExamBrowser.Monitoring.Contracts.Applications; +using SafeExamBrowser.WindowsApi.Contracts.Events; + +namespace SafeExamBrowser.Proctoring.ScreenProctoring.Data +{ + internal class MetaDataAggregator + { + private readonly IApplicationMonitor applicationMonitor; + private readonly IBrowserApplication browser; + private readonly ILogger logger; + + private string applicationInfo; + private string browserInfo; + private TimeSpan elapsed; + private string triggerInfo; + private string urls; + private string windowTitle; + + internal MetaData Data => new MetaData + { + ApplicationInfo = applicationInfo, + BrowserInfo = browserInfo, + Elapsed = elapsed, + TriggerInfo = triggerInfo, + Urls = urls, + WindowTitle = windowTitle + }; + + internal MetaDataAggregator(IApplicationMonitor applicationMonitor, IBrowserApplication browser, TimeSpan elapsed, ILogger logger) + { + this.applicationMonitor = applicationMonitor; + this.browser = browser; + this.elapsed = elapsed; + this.logger = logger; + } + + internal void Capture(IntervalTrigger interval = default, KeyboardTrigger keyboard = default, MouseTrigger mouse = default) + { + CaptureApplicationData(); + CaptureBrowserData(); + + if (interval != default) + { + CaptureIntervalTrigger(interval); + } + else if (keyboard != default) + { + CaptureKeyboardTrigger(keyboard); + } + else if (mouse != default) + { + CaptureMouseTrigger(mouse); + } + + // TODO: Can only log URLs when allowed by policy in browser configuration! + logger.Debug($"Captured metadata: {applicationInfo} / {browserInfo} / {triggerInfo} / {urls} / {windowTitle}."); + } + + private void CaptureApplicationData() + { + if (applicationMonitor.TryGetActiveApplication(out var application)) + { + applicationInfo = BuildApplicationInfo(application); + windowTitle = string.IsNullOrEmpty(application.Window.Title) ? "-" : application.Window.Title; + } + else + { + applicationInfo = "-"; + windowTitle = "-"; + } + } + + private void CaptureBrowserData() + { + var windows = browser.GetWindows(); + + browserInfo = string.Join(", ", windows.Select(w => $"{(w.IsMainWindow ? "Main" : "Additional")} Window: {w.Title} ({w.Url})")); + urls = string.Join(", ", windows.Select(w => w.Url)); + } + + private void CaptureIntervalTrigger(IntervalTrigger interval) + { + triggerInfo = $"Maximum interval of {interval.ConfigurationValue}ms has been reached."; + } + + private void CaptureKeyboardTrigger(KeyboardTrigger keyboard) + { + var flags = Enum.GetValues(typeof(KeyModifier)).OfType().Where(m => m != KeyModifier.None && keyboard.Modifier.HasFlag(m)); + var modifiers = flags.Any() ? string.Join(" + ", flags) + " + " : string.Empty; + + triggerInfo = $"'{modifiers}{keyboard.Key}' has been {keyboard.State.ToString().ToLower()}."; + } + + private void CaptureMouseTrigger(MouseTrigger mouse) + { + if (mouse.Info.IsTouch) + { + triggerInfo = $"Tap as {mouse.Button} mouse button has been {mouse.State.ToString().ToLower()} at ({mouse.Info.X}/{mouse.Info.Y})."; + } + else + { + triggerInfo = $"{mouse.Button} mouse button has been {mouse.State.ToString().ToLower()} at ({mouse.Info.X}/{mouse.Info.Y})."; + } + } + + private string BuildApplicationInfo(ActiveApplication application) + { + var info = new StringBuilder(); + + info.Append(application.Process.Name); + + if (application.Process.OriginalName != default) + { + info.Append($" ({application.Process.OriginalName}{(application.Process.Signature == default ? ")" : "")}"); + } + + if (application.Process.Signature != default) + { + info.Append($"{(application.Process.OriginalName == default ? "(" : ", ")}{application.Process.Signature})"); + } + + return info.ToString(); + } + } +} diff --git a/SafeExamBrowser.Proctoring/ScreenProctoring/Data/Metadata.cs b/SafeExamBrowser.Proctoring/ScreenProctoring/Data/Metadata.cs index b64f7090..1536ac2d 100644 --- a/SafeExamBrowser.Proctoring/ScreenProctoring/Data/Metadata.cs +++ b/SafeExamBrowser.Proctoring/ScreenProctoring/Data/Metadata.cs @@ -7,60 +7,20 @@ */ using System; -using System.Linq; -using System.Text; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using SafeExamBrowser.Browser.Contracts; -using SafeExamBrowser.Logging.Contracts; -using SafeExamBrowser.Monitoring.Contracts.Applications; using SafeExamBrowser.Proctoring.ScreenProctoring.Service.Requests; -using SafeExamBrowser.WindowsApi.Contracts.Events; namespace SafeExamBrowser.Proctoring.ScreenProctoring.Data { - internal class Metadata + internal class MetaData { - private readonly IApplicationMonitor applicationMonitor; - private readonly IBrowserApplication browser; - private readonly ILogger logger; - - internal string ApplicationInfo { get; private set; } - internal string BrowserInfo { get; private set; } - internal TimeSpan Elapsed { get; private set; } - internal string TriggerInfo { get; private set; } - internal string Urls { get; private set; } - internal string WindowTitle { get; private set; } - - internal Metadata(IApplicationMonitor applicationMonitor, IBrowserApplication browser, TimeSpan elapsed, ILogger logger) - { - this.applicationMonitor = applicationMonitor; - this.browser = browser; - this.Elapsed = elapsed; - this.logger = logger; - } - - internal void Capture(IntervalTrigger interval = default, KeyboardTrigger keyboard = default, MouseTrigger mouse = default) - { - CaptureApplicationData(); - CaptureBrowserData(); - - if (interval != default) - { - CaptureIntervalTrigger(interval); - } - else if (keyboard != default) - { - CaptureKeyboardTrigger(keyboard); - } - else if (mouse != default) - { - CaptureMouseTrigger(mouse); - } - - // TODO: Can only log URLs when allowed by policy in browser configuration! - logger.Debug($"Captured metadata: {ApplicationInfo} / {BrowserInfo} / {TriggerInfo} / {Urls} / {WindowTitle}."); - } + internal string ApplicationInfo { get; set; } + internal string BrowserInfo { get; set; } + internal TimeSpan Elapsed { get; set; } + internal string TriggerInfo { get; set; } + internal string Urls { get; set; } + internal string WindowTitle { get; set; } internal string ToJson() { @@ -75,71 +35,5 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring.Data return json.ToString(Formatting.None); } - - private void CaptureApplicationData() - { - if (applicationMonitor.TryGetActiveApplication(out var application)) - { - ApplicationInfo = BuildApplicationInfo(application); - WindowTitle = string.IsNullOrEmpty(application.Window.Title) ? "-" : application.Window.Title; - } - else - { - ApplicationInfo = "-"; - WindowTitle = "-"; - } - } - - private void CaptureBrowserData() - { - var windows = browser.GetWindows(); - - BrowserInfo = string.Join(", ", windows.Select(w => $"{(w.IsMainWindow ? "Main" : "Additional")} Window: {w.Title} ({w.Url})")); - Urls = string.Join(", ", windows.Select(w => w.Url)); - } - - private void CaptureIntervalTrigger(IntervalTrigger interval) - { - TriggerInfo = $"Maximum interval of {interval.ConfigurationValue}ms has been reached ({interval.TimeElapsed}ms)."; - } - - private void CaptureKeyboardTrigger(KeyboardTrigger keyboard) - { - var flags = Enum.GetValues(typeof(KeyModifier)).OfType().Where(m => m != KeyModifier.None && keyboard.Modifier.HasFlag(m)); - var modifiers = flags.Any() ? string.Join(" + ", flags) + " + " : string.Empty; - - TriggerInfo = $"'{modifiers}{keyboard.Key}' has been {keyboard.State.ToString().ToLower()}."; - } - - private void CaptureMouseTrigger(MouseTrigger mouse) - { - if (mouse.Info.IsTouch) - { - TriggerInfo = $"Tap as {mouse.Button} mouse button has been {mouse.State.ToString().ToLower()} at ({mouse.Info.X}/{mouse.Info.Y})."; - } - else - { - TriggerInfo = $"{mouse.Button} mouse button has been {mouse.State.ToString().ToLower()} at ({mouse.Info.X}/{mouse.Info.Y})."; - } - } - - private string BuildApplicationInfo(ActiveApplication application) - { - var info = new StringBuilder(); - - info.Append(application.Process.Name); - - if (application.Process.OriginalName != default) - { - info.Append($" ({application.Process.OriginalName}{(application.Process.Signature == default ? ")" : "")}"); - } - - if (application.Process.Signature != default) - { - info.Append($"{(application.Process.OriginalName == default ? "(" : ", ")}{application.Process.Signature})"); - } - - return info.ToString(); - } } } diff --git a/SafeExamBrowser.Proctoring/ScreenProctoring/DataCollector.cs b/SafeExamBrowser.Proctoring/ScreenProctoring/DataCollector.cs index eddbffcf..f9809358 100644 --- a/SafeExamBrowser.Proctoring/ScreenProctoring/DataCollector.cs +++ b/SafeExamBrowser.Proctoring/ScreenProctoring/DataCollector.cs @@ -115,7 +115,6 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring var trigger = new IntervalTrigger { ConfigurationValue = settings.MaxInterval, - TimeElapsed = Convert.ToInt32(DateTime.Now.Subtract(last).TotalMilliseconds) }; TryCollect(interval: trigger); @@ -148,14 +147,16 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring { try { - var metadata = new Metadata(applicationMonitor, browser, elapsed, logger.CloneFor(nameof(Metadata))); - var screenShot = new ScreenShot(logger.CloneFor(nameof(ScreenShot)), settings); + var metaData = new MetaDataAggregator(applicationMonitor, browser, elapsed, logger.CloneFor(nameof(MetaDataAggregator))); + var screenShot = new ScreenShotProcessor(logger.CloneFor(nameof(ScreenShotProcessor)), settings); - metadata.Capture(interval, keyboard, mouse); + metaData.Capture(interval, keyboard, mouse); screenShot.Take(); screenShot.Compress(); - DataCollected?.Invoke(metadata, screenShot); + DataCollected?.Invoke(metaData.Data, screenShot.Data); + + screenShot.Dispose(); } catch (Exception e) { diff --git a/SafeExamBrowser.Proctoring/ScreenProctoring/Events/DataCollectedEventHandler.cs b/SafeExamBrowser.Proctoring/ScreenProctoring/Events/DataCollectedEventHandler.cs index 5bc3d9eb..af86ec32 100644 --- a/SafeExamBrowser.Proctoring/ScreenProctoring/Events/DataCollectedEventHandler.cs +++ b/SafeExamBrowser.Proctoring/ScreenProctoring/Events/DataCollectedEventHandler.cs @@ -11,5 +11,5 @@ using SafeExamBrowser.Proctoring.ScreenProctoring.Imaging; namespace SafeExamBrowser.Proctoring.ScreenProctoring.Events { - internal delegate void DataCollectedEventHandler(Metadata metadata, ScreenShot screenShot); + internal delegate void DataCollectedEventHandler(MetaData metaData, ScreenShot screenShot); } diff --git a/SafeExamBrowser.Proctoring/ScreenProctoring/Imaging/ScreenShot.cs b/SafeExamBrowser.Proctoring/ScreenProctoring/Imaging/ScreenShot.cs index 561517db..173b71e9 100644 --- a/SafeExamBrowser.Proctoring/ScreenProctoring/Imaging/ScreenShot.cs +++ b/SafeExamBrowser.Proctoring/ScreenProctoring/Imaging/ScreenShot.cs @@ -7,146 +7,26 @@ */ using System; -using System.Drawing; -using System.Drawing.Imaging; -using System.IO; -using System.Linq; -using System.Windows.Forms; -using KGySoft.Drawing; -using KGySoft.Drawing.Imaging; -using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.Settings.Proctoring; -using ImageFormat = SafeExamBrowser.Settings.Proctoring.ImageFormat; namespace SafeExamBrowser.Proctoring.ScreenProctoring.Imaging { internal class ScreenShot : IDisposable { - private readonly ILogger logger; - private readonly ScreenProctoringSettings settings; - - internal Bitmap Bitmap { get; private set; } - internal byte[] Data { get; private set; } - internal ImageFormat Format { get; private set; } - internal int Height { get; private set; } - internal int Width { get; private set; } - - public ScreenShot(ILogger logger, ScreenProctoringSettings settings) - { - this.logger = logger; - this.settings = settings; - } + internal DateTime CaptureTime { get; set; } + internal byte[] Data { get; set; } + internal ImageFormat Format { get; set; } + internal int Height { get; set; } + internal int Width { get; set; } public void Dispose() { - Bitmap?.Dispose(); - Bitmap = default; Data = default; } - public string ToReducedString() - { - 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()}"; - } - - internal void Compress() - { - var order = ProcessingOrder.QuantizingDownscaling; - var original = ToReducedString(); - var parameters = $"{order}, {settings.ImageQuantization}, 1:{settings.ImageDownscaling}"; - - switch (order) - { - case ProcessingOrder.DownscalingQuantizing: - Downscale(); - Quantize(); - Serialize(); - break; - case ProcessingOrder.QuantizingDownscaling: - Quantize(); - Downscale(); - Serialize(); - break; - } - - logger.Debug($"Compressed from '{original}' to '{ToReducedString()}' ({parameters})."); - } - - internal void Take() - { - var x = Screen.AllScreens.Min(s => s.Bounds.X); - var y = Screen.AllScreens.Min(s => s.Bounds.Y); - var width = Screen.AllScreens.Max(s => s.Bounds.X + s.Bounds.Width) - x; - var height = Screen.AllScreens.Max(s => s.Bounds.Y + s.Bounds.Height) - y; - - Bitmap = new Bitmap(width, height, PixelFormat.Format24bppRgb); - Format = settings.ImageFormat; - Height = height; - Width = width; - - using (var graphics = Graphics.FromImage(Bitmap)) - { - graphics.CopyFromScreen(x, y, 0, 0, new Size(width, height), CopyPixelOperation.SourceCopy); - graphics.DrawCursorPosition(); - } - - Serialize(); - } - - private void Downscale() - { - if (settings.ImageDownscaling > 1) - { - Height = Convert.ToInt32(Height / settings.ImageDownscaling); - Width = Convert.ToInt32(Width / settings.ImageDownscaling); - - var downscaled = new Bitmap(Width, Height, Bitmap.PixelFormat); - - Bitmap.DrawInto(downscaled, new Rectangle(0, 0, Width, Height), ScalingMode.NearestNeighbor); - Bitmap.Dispose(); - Bitmap = downscaled; - } - } - - private void Quantize() - { - var ditherer = settings.ImageDownscaling > 1 ? OrderedDitherer.Bayer2x2 : default; - var pixelFormat = settings.ImageQuantization.ToPixelFormat(); - var quantizer = settings.ImageQuantization.ToQuantizer(); - - Bitmap = Bitmap.ConvertPixelFormat(pixelFormat, quantizer, ditherer); - } - - private void Serialize() - { - using (var memoryStream = new MemoryStream()) - { - if (Format == ImageFormat.Jpg) - { - SerializeJpg(memoryStream); - } - else - { - Bitmap.Save(memoryStream, Format.ToSystemFormat()); - } - - Data = memoryStream.ToArray(); - } - } - - private void SerializeJpg(MemoryStream memoryStream) - { - var codec = ImageCodecInfo.GetImageEncoders().First(c => c.FormatID == System.Drawing.Imaging.ImageFormat.Jpeg.Guid); - var parameters = new EncoderParameters(1); - var quality = 100; - - parameters.Param[0] = new EncoderParameter(Encoder.Quality, quality); - Bitmap.Save(memoryStream, codec, parameters); + return $"captured: {CaptureTime}, format: {Format.ToString().ToUpper()}, resolution: {Width}x{Height}, size: {Data.Length / 1000:N0}kB"; } } } diff --git a/SafeExamBrowser.Proctoring/ScreenProctoring/Imaging/ScreenShotProcessor.cs b/SafeExamBrowser.Proctoring/ScreenProctoring/Imaging/ScreenShotProcessor.cs new file mode 100644 index 00000000..208ca017 --- /dev/null +++ b/SafeExamBrowser.Proctoring/ScreenProctoring/Imaging/ScreenShotProcessor.cs @@ -0,0 +1,160 @@ +/* + * 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.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Linq; +using System.Windows.Forms; +using KGySoft.Drawing; +using KGySoft.Drawing.Imaging; +using SafeExamBrowser.Logging.Contracts; +using SafeExamBrowser.Settings.Proctoring; +using ImageFormat = SafeExamBrowser.Settings.Proctoring.ImageFormat; + +namespace SafeExamBrowser.Proctoring.ScreenProctoring.Imaging +{ + internal class ScreenShotProcessor : IDisposable + { + private readonly ILogger logger; + private readonly ScreenProctoringSettings settings; + + private Bitmap bitmap; + private DateTime captureTime; + private byte[] data; + private ImageFormat format; + private int height; + private int width; + + internal ScreenShot Data => new ScreenShot + { + CaptureTime = captureTime, + Data = data, + Format = format, + Height = height, + Width = width + }; + + public ScreenShotProcessor(ILogger logger, ScreenProctoringSettings settings) + { + this.logger = logger; + this.settings = settings; + } + + public void Dispose() + { + bitmap?.Dispose(); + bitmap = default; + data = default; + } + + internal void Compress() + { + var order = ProcessingOrder.QuantizingDownscaling; + var original = ToReducedString(); + var parameters = $"{order}, {settings.ImageQuantization}, 1:{settings.ImageDownscaling}"; + + switch (order) + { + case ProcessingOrder.DownscalingQuantizing: + Downscale(); + Quantize(); + Serialize(); + break; + case ProcessingOrder.QuantizingDownscaling: + Quantize(); + Downscale(); + Serialize(); + break; + } + + logger.Debug($"Compressed from '{original}' to '{ToReducedString()}' ({parameters})."); + } + + internal void Take() + { + var x = Screen.AllScreens.Min(s => s.Bounds.X); + var y = Screen.AllScreens.Min(s => s.Bounds.Y); + var width = Screen.AllScreens.Max(s => s.Bounds.X + s.Bounds.Width) - x; + var height = Screen.AllScreens.Max(s => s.Bounds.Y + s.Bounds.Height) - y; + + bitmap = new Bitmap(width, height, PixelFormat.Format24bppRgb); + captureTime = DateTime.Now; + format = settings.ImageFormat; + this.height = height; + this.width = width; + + using (var graphics = Graphics.FromImage(bitmap)) + { + graphics.CopyFromScreen(x, y, 0, 0, new Size(width, height), CopyPixelOperation.SourceCopy); + graphics.DrawCursorPosition(); + } + + Serialize(); + + logger.Debug($"Captured '{ToReducedString()}' at {captureTime}."); + } + + private void Downscale() + { + if (settings.ImageDownscaling > 1) + { + height = Convert.ToInt32(height / settings.ImageDownscaling); + width = Convert.ToInt32(width / settings.ImageDownscaling); + + var downscaled = new Bitmap(width, height, bitmap.PixelFormat); + + bitmap.DrawInto(downscaled, new Rectangle(0, 0, width, height), ScalingMode.NearestNeighbor); + bitmap.Dispose(); + bitmap = downscaled; + } + } + + private void Quantize() + { + var ditherer = settings.ImageDownscaling > 1 ? OrderedDitherer.Bayer2x2 : default; + var pixelFormat = settings.ImageQuantization.ToPixelFormat(); + var quantizer = settings.ImageQuantization.ToQuantizer(); + + bitmap = bitmap.ConvertPixelFormat(pixelFormat, quantizer, ditherer); + } + + private void Serialize() + { + using (var memoryStream = new MemoryStream()) + { + if (format == ImageFormat.Jpg) + { + SerializeJpg(memoryStream); + } + else + { + bitmap.Save(memoryStream, format.ToSystemFormat()); + } + + data = memoryStream.ToArray(); + } + } + + private void SerializeJpg(MemoryStream memoryStream) + { + var codec = ImageCodecInfo.GetImageEncoders().First(c => c.FormatID == System.Drawing.Imaging.ImageFormat.Jpeg.Guid); + var parameters = new EncoderParameters(1); + var quality = 100; + + parameters.Param[0] = new EncoderParameter(Encoder.Quality, quality); + bitmap.Save(memoryStream, codec, parameters); + } + + private string ToReducedString() + { + return $"{width}x{height}, {data.Length / 1000:N0}kB, {format.ToString().ToUpper()}"; + } + } +} diff --git a/SafeExamBrowser.Proctoring/ScreenProctoring/ScreenProctoringImplementation.cs b/SafeExamBrowser.Proctoring/ScreenProctoring/ScreenProctoringImplementation.cs index af3ba8ed..b10cdbe7 100644 --- a/SafeExamBrowser.Proctoring/ScreenProctoring/ScreenProctoringImplementation.cs +++ b/SafeExamBrowser.Proctoring/ScreenProctoring/ScreenProctoringImplementation.cs @@ -8,6 +8,7 @@ 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; @@ -36,6 +37,7 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring public override event NotificationChangedEventHandler NotificationChanged; internal ScreenProctoringImplementation( + AppConfig appConfig, IApplicationMonitor applicationMonitor, IBrowserApplication browser, IModuleLogger logger, @@ -48,7 +50,7 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring this.logger = logger; this.service = service; this.settings = settings.ScreenProctoring; - this.spooler = new TransmissionSpooler(logger.CloneFor(nameof(TransmissionSpooler)), service); + this.spooler = new TransmissionSpooler(appConfig, logger.CloneFor(nameof(TransmissionSpooler)), service); this.text = text; } @@ -138,9 +140,9 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring logger.Info("Terminated proctoring."); } - private void Collector_DataCollected(Metadata metadata, ScreenShot screenShot) + private void Collector_DataCollected(MetaData metaData, ScreenShot screenShot) { - spooler.Add(metadata, screenShot); + spooler.Add(metaData, screenShot); } private void Connect(string sessionId = default) diff --git a/SafeExamBrowser.Proctoring/ScreenProctoring/Service/Parser.cs b/SafeExamBrowser.Proctoring/ScreenProctoring/Service/Parser.cs index b7036eec..11ecc7b8 100644 --- a/SafeExamBrowser.Proctoring/ScreenProctoring/Service/Parser.cs +++ b/SafeExamBrowser.Proctoring/ScreenProctoring/Service/Parser.cs @@ -52,9 +52,16 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring.Service health = default; - if (response.Headers.TryGetValues(Header.HEALTH, out var values)) + try { - success = int.TryParse(values.First(), out health); + if (response.Headers.TryGetValues(Header.HEALTH, out var values)) + { + success = int.TryParse(values.First(), out health); + } + } + catch (Exception e) + { + logger.Error("Failed to parse health!", e); } return success; diff --git a/SafeExamBrowser.Proctoring/ScreenProctoring/Service/Requests/ScreenShotRequest.cs b/SafeExamBrowser.Proctoring/ScreenProctoring/Service/Requests/ScreenShotRequest.cs index 7a20d8e0..7b681e3a 100644 --- a/SafeExamBrowser.Proctoring/ScreenProctoring/Service/Requests/ScreenShotRequest.cs +++ b/SafeExamBrowser.Proctoring/ScreenProctoring/Service/Requests/ScreenShotRequest.cs @@ -21,10 +21,10 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring.Service.Requests { } - internal bool TryExecute(Metadata metadata, ScreenShot screenShot, string sessionId, out string message) + internal bool TryExecute(MetaData metaData, ScreenShot screenShot, string sessionId, out string message) { var imageFormat = (Header.IMAGE_FORMAT, ToString(screenShot.Format)); - var metdataJson = (Header.METADATA, metadata.ToJson()); + var metdataJson = (Header.METADATA, metaData.ToJson()); var timestamp = (Header.TIMESTAMP, DateTime.Now.ToUnixTimestamp().ToString()); var url = api.ScreenShotEndpoint.Replace(Api.SESSION_ID, sessionId); var success = TryExecute(HttpMethod.Post, url, out var response, screenShot.Data, ContentType.OCTET_STREAM, Authorization, imageFormat, metdataJson, timestamp); diff --git a/SafeExamBrowser.Proctoring/ScreenProctoring/Service/ServiceProxy.cs b/SafeExamBrowser.Proctoring/ScreenProctoring/Service/ServiceProxy.cs index edddad64..e08ac472 100644 --- a/SafeExamBrowser.Proctoring/ScreenProctoring/Service/ServiceProxy.cs +++ b/SafeExamBrowser.Proctoring/ScreenProctoring/Service/ServiceProxy.cs @@ -87,10 +87,10 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring.Service return new ServiceResponse(success, health, message); } - internal ServiceResponse Send(Metadata metadata, ScreenShot screenShot) + internal ServiceResponse Send(MetaData metaData, ScreenShot screenShot) { var request = new ScreenShotRequest(api, httpClient, logger, parser); - var success = request.TryExecute(metadata, screenShot, SessionId, out var message); + var success = request.TryExecute(metaData, screenShot, SessionId, out var message); if (success) { diff --git a/SafeExamBrowser.Proctoring/ScreenProctoring/TransmissionSpooler.cs b/SafeExamBrowser.Proctoring/ScreenProctoring/TransmissionSpooler.cs index 4a1129a3..444c10c9 100644 --- a/SafeExamBrowser.Proctoring/ScreenProctoring/TransmissionSpooler.cs +++ b/SafeExamBrowser.Proctoring/ScreenProctoring/TransmissionSpooler.cs @@ -12,6 +12,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Timers; +using SafeExamBrowser.Configuration.Contracts; using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.Proctoring.ScreenProctoring.Data; using SafeExamBrowser.Proctoring.ScreenProctoring.Imaging; @@ -25,32 +26,34 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring const int BAD = 10; const int GOOD = 0; + private readonly Cache cache; private readonly ILogger logger; - private readonly ConcurrentQueue<(Metadata metadata, ScreenShot screenShot)> queue; + private readonly ConcurrentQueue<(MetaData metaData, ScreenShot screenShot)> queue; private readonly Random random; private readonly ServiceProxy service; private readonly Timer timer; - private Queue<(Metadata metadata, DateTime schedule, ScreenShot screenShot)> buffer; + private Queue<(MetaData metaData, DateTime schedule, ScreenShot screenShot)> buffer; private int health; private bool recovering; private DateTime resume; private Thread thread; private CancellationTokenSource token; - internal TransmissionSpooler(ILogger logger, ServiceProxy service) + internal TransmissionSpooler(AppConfig appConfig, IModuleLogger logger, ServiceProxy service) { - this.buffer = new Queue<(Metadata, DateTime, ScreenShot)>(); + this.buffer = new Queue<(MetaData, DateTime, ScreenShot)>(); + this.cache = new Cache(appConfig, logger.CloneFor(nameof(Cache))); this.logger = logger; - this.queue = new ConcurrentQueue<(Metadata, ScreenShot)>(); + this.queue = new ConcurrentQueue<(MetaData, ScreenShot)>(); this.random = new Random(); this.service = service; this.timer = new Timer(); } - internal void Add(Metadata metadata, ScreenShot screenShot) + internal void Add(MetaData metaData, ScreenShot screenShot) { - queue.Enqueue((metadata, screenShot)); + queue.Enqueue((metaData, screenShot)); } internal void Start() @@ -68,9 +71,10 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring thread.IsBackground = true; thread.Start(); + // TODO: Only use timer when BAD, until then read health from transmission response! timer.AutoReset = false; timer.Elapsed += Timer_Elapsed; - timer.Interval = 2000; + timer.Interval = 10000; // TODO: Revert! // timer.Interval = FIFTEEN_SECONDS; timer.Start(); @@ -93,17 +97,15 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring } catch (Exception e) { - logger.Error("Failed to initiate cancellation!", e); + logger.Error("Failed to initiate execution cancellation!", e); } try { - var success = thread.Join(TEN_SECONDS); - - if (!success) + if (!thread.Join(TEN_SECONDS)) { thread.Abort(); - logger.Warn($"Aborted since stopping gracefully within {TEN_SECONDS / 1000:N0} seconds failed!"); + logger.Warn($"Aborted execution since stopping gracefully within {TEN_SECONDS / 1000} seconds failed!"); } } catch (Exception e) @@ -125,7 +127,7 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring { if (health == BAD) { - ExecuteCacheOnly(); + ExecuteCaching(); } else if (recovering) { @@ -146,7 +148,7 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring logger.Debug("Stopped."); } - private void ExecuteCacheOnly() + private void ExecuteCaching() { const int THREE_MINUTES = 180; @@ -158,18 +160,15 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring logger.Warn($"Activating local caching and suspending transmission due to bad service health (value: {health}, resume: {resume:HH:mm:ss})."); } - CacheBuffer(); + CacheFromBuffer(); CacheFromQueue(); } private void ExecuteDeferred() { - Schedule(health); - - if (TryPeekFromBuffer(out _, out var schedule, out _) && schedule <= DateTime.Now) - { - TryTransmitFromBuffer(); - } + BufferFromCache(); + BufferFromQueue(); + TryTransmitFromBuffer(); } private void ExecuteNormally() @@ -181,19 +180,22 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring private void ExecuteRecovery() { - CacheFromQueue(); recovering = DateTime.Now < resume; - if (!recovering) + if (recovering) + { + CacheFromQueue(); + } + else { logger.Info($"Deactivating local caching and resuming transmission due to improved service health (value: {health})."); } } - private void Buffer(Metadata metadata, DateTime schedule, ScreenShot screenShot) + 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)); + buffer.Enqueue((metaData, schedule, screenShot)); + buffer = new Queue<(MetaData, DateTime, ScreenShot)>(buffer.OrderBy((b) => b.schedule)); // TODO: Remove! PrintBuffer(); @@ -202,111 +204,117 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring private void PrintBuffer() { logger.Log("-------------------------------------------------------------------------------------------------------"); - logger.Info($"Buffer: {buffer.Count}"); + logger.Info(""); + logger.Log($"\t\t\t\tBuffer: {buffer.Count} items"); foreach (var (m, t, s) in buffer) { - logger.Log($"\t\t{t} ({m.Elapsed} {s.Data.Length})"); + logger.Log($"\t\t\t\t{s.CaptureTime:HH:mm:ss} -> {t:HH:mm:ss} ({m.Elapsed} {s.Data.Length / 1000:N0}kB)"); } logger.Log("-------------------------------------------------------------------------------------------------------"); } - private void CacheBuffer() + private void BufferFromCache() { - foreach (var (metadata, _, screenShot) in buffer) + if (cache.TryDequeue(out var metaData, out var screenShot)) { - using (screenShot) - { - Cache(metadata, screenShot); - } - } - - // TODO: Revert! - // buffer.Clear(); - } - - private void CacheFromQueue() - { - if (TryDequeue(out var metadata, out var screenShot)) - { - using (screenShot) - { - Cache(metadata, screenShot); - } + Buffer(metaData, CalculateSchedule(metaData), screenShot); } } - private void Cache(Metadata metadata, ScreenShot screenShot) + private void BufferFromQueue() { - // TODO: Implement caching! - //var directory = Dispatcher.Invoke(() => OutputPath.Text); - //var extension = screenShot.Format.ToString().ToLower(); - //var path = Path.Combine(directory, $"{DateTime.Now:HH\\hmm\\mss\\sfff\\m\\s}.{extension}"); - - //if (!Directory.Exists(directory)) - //{ - // Directory.CreateDirectory(directory); - // logger.Debug($"Created local output directory '{directory}'."); - //} - - //File.WriteAllBytes(path, screenShot.Data); - //logger.Debug($"Screen shot saved as '{path}'."); - } - - private void Schedule(int health) - { - if (TryDequeue(out var metadata, out var screenShot)) + if (TryDequeue(out var metaData, out var screenShot)) { - var schedule = DateTime.Now.AddMilliseconds((health + 1) * metadata.Elapsed.TotalMilliseconds); - - Buffer(metadata, schedule, screenShot); + Buffer(metaData, CalculateSchedule(metaData), screenShot); } } - private bool TryDequeue(out Metadata metadata, out ScreenShot screenShot) + private void CacheFromBuffer() { - metadata = default; - screenShot = default; - - if (queue.TryDequeue(out var item)) + if (TryPeekFromBuffer(out var metaData, out _, out var screenShot)) { - metadata = item.metadata; - screenShot = item.screenShot; - } - - 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 _, out var screenShot)) - { - // TODO: Exception after sending of screenshot, most likely due to concurrent disposal!! - success = TryTransmit(metadata, screenShot); + var success = cache.TryEnqueue(metaData, screenShot); if (success) { buffer.Dequeue(); screenShot.Dispose(); - // TODO: Revert! + // TODO: Remove! + PrintBuffer(); + } + } + } + + private void CacheFromQueue() + { + if (TryDequeue(out var metaData, out var screenShot)) + { + var success = cache.TryEnqueue(metaData, screenShot); + + if (success) + { + screenShot.Dispose(); + } + else + { + Buffer(metaData, DateTime.Now, screenShot); + } + } + } + + private DateTime CalculateSchedule(MetaData metaData) + { + var timeout = (health + 1) * metaData.Elapsed.TotalMilliseconds; + var schedule = DateTime.Now.AddMilliseconds(timeout); + + return schedule; + } + + private bool TryDequeue(out MetaData metaData, out ScreenShot screenShot) + { + metaData = default; + screenShot = default; + + if (queue.TryDequeue(out var item)) + { + metaData = item.metaData; + screenShot = item.screenShot; + } + + 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(); + + // TODO: Remove! PrintBuffer(); } } @@ -316,17 +324,21 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring private bool TryTransmitFromCache() { - var success = false; + var success = true; - // TODO: Implement transmission from cache! - //if (Cache.Any()) - //{ - // - //} - //else - //{ - // 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; } @@ -335,9 +347,9 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring { var success = false; - if (TryDequeue(out var metadata, out var screenShot)) + if (TryDequeue(out var metaData, out var screenShot)) { - success = TryTransmit(metadata, screenShot); + success = TryTransmit(metaData, screenShot); if (success) { @@ -345,20 +357,20 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring } else { - Buffer(metadata, DateTime.Now, screenShot); + Buffer(metaData, DateTime.Now, screenShot); } } return success; } - private bool TryTransmit(Metadata metadata, ScreenShot screenShot) + private bool TryTransmit(MetaData metaData, ScreenShot screenShot) { var success = false; if (service.IsConnected) { - success = service.Send(metadata, screenShot).Success; + success = service.Send(metaData, screenShot).Success; } else { @@ -368,7 +380,8 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring return success; } - private readonly Random temp = new Random(); + private int factor = 2; + private int bads = 0; private void Timer_Elapsed(object sender, ElapsedEventArgs e) { @@ -396,12 +409,26 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring var previous = health; - health += temp.Next(-3, 5); - health = health < GOOD ? GOOD : (health > BAD ? BAD : health); + if (bads < 2) + { + bads += health == BAD ? 1 : 0; + factor = health == BAD ? -2 : (health == GOOD ? 2 : factor); + health += factor; + health = health < GOOD ? GOOD : (health > BAD ? BAD : health); + } + else + { + health = 0; + } if (previous != health) { - logger.Info($"Service health {(previous < health ? "deteriorated" : "improved")} from {previous} to {health}."); + logger.Warn($"Service health {(previous < health ? "deteriorated" : "improved")} from {previous} to {health}."); + + if (bads >= 2 && health == 0) + { + logger.Warn("Stopped health simulation."); + } } timer.Start(); diff --git a/SafeExamBrowser.Server/Extensions.cs b/SafeExamBrowser.Server/Extensions.cs index fe03fff8..af257783 100644 --- a/SafeExamBrowser.Server/Extensions.cs +++ b/SafeExamBrowser.Server/Extensions.cs @@ -14,6 +14,11 @@ namespace SafeExamBrowser.Server { internal static class Extensions { + internal static string ToLogString(this HttpResponseMessage response) + { + return $"{(int?) response?.StatusCode} {response?.StatusCode} {response?.ReasonPhrase}"; + } + internal static string ToLogType(this LogLevel severity) { switch (severity) @@ -31,11 +36,6 @@ namespace SafeExamBrowser.Server return "UNKNOWN"; } - internal static string ToLogString(this HttpResponseMessage response) - { - return $"{(int?) response?.StatusCode} {response?.StatusCode} {response?.ReasonPhrase}"; - } - internal static long ToUnixTimestamp(this DateTime date) { return new DateTimeOffset(date).ToUnixTimeMilliseconds(); diff --git a/SafeExamBrowser.Server/Requests/ExamConfigurationRequest.cs b/SafeExamBrowser.Server/Requests/ExamConfigurationRequest.cs index e9375260..b539d92f 100644 --- a/SafeExamBrowser.Server/Requests/ExamConfigurationRequest.cs +++ b/SafeExamBrowser.Server/Requests/ExamConfigurationRequest.cs @@ -30,7 +30,7 @@ namespace SafeExamBrowser.Server.Requests var url = $"{api.ConfigurationEndpoint}?examId={exam.Id}"; var success = TryExecute(HttpMethod.Get, url, out var response, default, default, Authorization, Token); - content = response.Content; + content = response?.Content; message = response.ToLogString(); return success; diff --git a/SafeExamBrowser.Server/Requests/PingRequest.cs b/SafeExamBrowser.Server/Requests/PingRequest.cs index dfe9e770..73a74671 100644 --- a/SafeExamBrowser.Server/Requests/PingRequest.cs +++ b/SafeExamBrowser.Server/Requests/PingRequest.cs @@ -36,7 +36,7 @@ namespace SafeExamBrowser.Server.Requests var success = TryExecute(HttpMethod.Post, api.PingEndpoint, out var response, requestContent, ContentType.URL_ENCODED, Authorization, Token); - content = response.Content; + content = response?.Content; message = response.ToLogString(); return success;