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;