SEBSP-23: Finished basic network redundancy. This build contains a service health simulation.

This commit is contained in:
Damian Büchel 2024-02-21 18:37:23 +01:00
parent a213ec0f7d
commit f902ee9598
18 changed files with 744 additions and 389 deletions

View file

@ -69,7 +69,7 @@ namespace SafeExamBrowser.Proctoring
var logger = this.logger.CloneFor(nameof(ScreenProctoring)); var logger = this.logger.CloneFor(nameof(ScreenProctoring));
var service = new ServiceProxy(logger.CloneFor(nameof(ServiceProxy))); 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; return implementations;

View file

@ -82,6 +82,8 @@
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
<Reference Include="System.Windows.Forms" /> <Reference Include="System.Windows.Forms" />
<Reference Include="System.Xaml" /> <Reference Include="System.Xaml" />
<Reference Include="System.Xml" />
<Reference Include="System.Xml.Linq" />
<Reference Include="WindowsBase" /> <Reference Include="WindowsBase" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -91,14 +93,17 @@
<Compile Include="ProctoringFactory.cs" /> <Compile Include="ProctoringFactory.cs" />
<Compile Include="ProctoringImplementation.cs" /> <Compile Include="ProctoringImplementation.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ScreenProctoring\Cache.cs" />
<Compile Include="ScreenProctoring\Data\IntervalTrigger.cs" /> <Compile Include="ScreenProctoring\Data\IntervalTrigger.cs" />
<Compile Include="ScreenProctoring\Data\KeyboardTrigger.cs" /> <Compile Include="ScreenProctoring\Data\KeyboardTrigger.cs" />
<Compile Include="ScreenProctoring\Data\MetaData.cs" />
<Compile Include="ScreenProctoring\Data\MouseTrigger.cs" /> <Compile Include="ScreenProctoring\Data\MouseTrigger.cs" />
<Compile Include="ScreenProctoring\Events\DataCollectedEventHandler.cs" /> <Compile Include="ScreenProctoring\Events\DataCollectedEventHandler.cs" />
<Compile Include="ScreenProctoring\Imaging\Extensions.cs" /> <Compile Include="ScreenProctoring\Imaging\Extensions.cs" />
<Compile Include="ScreenProctoring\Data\Metadata.cs" /> <Compile Include="ScreenProctoring\Data\MetaDataAggregator.cs" />
<Compile Include="ScreenProctoring\Imaging\ProcessingOrder.cs" /> <Compile Include="ScreenProctoring\Imaging\ProcessingOrder.cs" />
<Compile Include="ScreenProctoring\Imaging\ScreenShot.cs" /> <Compile Include="ScreenProctoring\Imaging\ScreenShot.cs" />
<Compile Include="ScreenProctoring\Imaging\ScreenShotProcessor.cs" />
<Compile Include="ScreenProctoring\ScreenProctoringImplementation.cs" /> <Compile Include="ScreenProctoring\ScreenProctoringImplementation.cs" />
<Compile Include="ScreenProctoring\Service\Api.cs" /> <Compile Include="ScreenProctoring\Service\Api.cs" />
<Compile Include="ScreenProctoring\DataCollector.cs" /> <Compile Include="ScreenProctoring\DataCollector.cs" />

View file

@ -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);
}
}
}

View file

@ -11,6 +11,5 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring.Data
internal class IntervalTrigger internal class IntervalTrigger
{ {
public int ConfigurationValue { get; internal set; } public int ConfigurationValue { get; internal set; }
public int TimeElapsed { get; internal set; }
} }
} }

View file

@ -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<KeyModifier>().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();
}
}
}

View file

@ -7,60 +7,20 @@
*/ */
using System; using System;
using System.Linq;
using System.Text;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; 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.Proctoring.ScreenProctoring.Service.Requests;
using SafeExamBrowser.WindowsApi.Contracts.Events;
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Data namespace SafeExamBrowser.Proctoring.ScreenProctoring.Data
{ {
internal class Metadata internal class MetaData
{ {
private readonly IApplicationMonitor applicationMonitor; internal string ApplicationInfo { get; set; }
private readonly IBrowserApplication browser; internal string BrowserInfo { get; set; }
private readonly ILogger logger; internal TimeSpan Elapsed { get; set; }
internal string TriggerInfo { get; set; }
internal string ApplicationInfo { get; private set; } internal string Urls { get; set; }
internal string BrowserInfo { get; private set; } internal string WindowTitle { get; 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 ToJson() internal string ToJson()
{ {
@ -75,71 +35,5 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring.Data
return json.ToString(Formatting.None); 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<KeyModifier>().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();
}
} }
} }

View file

@ -115,7 +115,6 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
var trigger = new IntervalTrigger var trigger = new IntervalTrigger
{ {
ConfigurationValue = settings.MaxInterval, ConfigurationValue = settings.MaxInterval,
TimeElapsed = Convert.ToInt32(DateTime.Now.Subtract(last).TotalMilliseconds)
}; };
TryCollect(interval: trigger); TryCollect(interval: trigger);
@ -148,14 +147,16 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
{ {
try try
{ {
var metadata = new Metadata(applicationMonitor, browser, elapsed, logger.CloneFor(nameof(Metadata))); var metaData = new MetaDataAggregator(applicationMonitor, browser, elapsed, logger.CloneFor(nameof(MetaDataAggregator)));
var screenShot = new ScreenShot(logger.CloneFor(nameof(ScreenShot)), settings); var screenShot = new ScreenShotProcessor(logger.CloneFor(nameof(ScreenShotProcessor)), settings);
metadata.Capture(interval, keyboard, mouse); metaData.Capture(interval, keyboard, mouse);
screenShot.Take(); screenShot.Take();
screenShot.Compress(); screenShot.Compress();
DataCollected?.Invoke(metadata, screenShot); DataCollected?.Invoke(metaData.Data, screenShot.Data);
screenShot.Dispose();
} }
catch (Exception e) catch (Exception e)
{ {

View file

@ -11,5 +11,5 @@ using SafeExamBrowser.Proctoring.ScreenProctoring.Imaging;
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Events namespace SafeExamBrowser.Proctoring.ScreenProctoring.Events
{ {
internal delegate void DataCollectedEventHandler(Metadata metadata, ScreenShot screenShot); internal delegate void DataCollectedEventHandler(MetaData metaData, ScreenShot screenShot);
} }

View file

@ -7,146 +7,26 @@
*/ */
using System; 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 SafeExamBrowser.Settings.Proctoring;
using ImageFormat = SafeExamBrowser.Settings.Proctoring.ImageFormat;
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Imaging namespace SafeExamBrowser.Proctoring.ScreenProctoring.Imaging
{ {
internal class ScreenShot : IDisposable internal class ScreenShot : IDisposable
{ {
private readonly ILogger logger; internal DateTime CaptureTime { get; set; }
private readonly ScreenProctoringSettings settings; internal byte[] Data { get; set; }
internal ImageFormat Format { get; set; }
internal Bitmap Bitmap { get; private set; } internal int Height { get; set; }
internal byte[] Data { get; private set; } internal int Width { get; 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;
}
public void Dispose() public void Dispose()
{ {
Bitmap?.Dispose();
Bitmap = default;
Data = default; Data = default;
} }
public string ToReducedString()
{
return $"{Width}x{Height}, {Data.Length / 1000:N0}kB, {Format.ToString().ToUpper()}";
}
public override string ToString() public override string ToString()
{ {
return $"resolution: {Width}x{Height}, size: {Data.Length / 1000:N0}kB, format: {Format.ToString().ToUpper()}"; return $"captured: {CaptureTime}, format: {Format.ToString().ToUpper()}, resolution: {Width}x{Height}, size: {Data.Length / 1000:N0}kB";
}
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);
} }
} }
} }

View file

@ -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()}";
}
}
}

View file

@ -8,6 +8,7 @@
using System; using System;
using SafeExamBrowser.Browser.Contracts; using SafeExamBrowser.Browser.Contracts;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Core.Contracts.Notifications.Events; using SafeExamBrowser.Core.Contracts.Notifications.Events;
using SafeExamBrowser.Core.Contracts.Resources.Icons; using SafeExamBrowser.Core.Contracts.Resources.Icons;
using SafeExamBrowser.I18n.Contracts; using SafeExamBrowser.I18n.Contracts;
@ -36,6 +37,7 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
public override event NotificationChangedEventHandler NotificationChanged; public override event NotificationChangedEventHandler NotificationChanged;
internal ScreenProctoringImplementation( internal ScreenProctoringImplementation(
AppConfig appConfig,
IApplicationMonitor applicationMonitor, IApplicationMonitor applicationMonitor,
IBrowserApplication browser, IBrowserApplication browser,
IModuleLogger logger, IModuleLogger logger,
@ -48,7 +50,7 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
this.logger = logger; this.logger = logger;
this.service = service; this.service = service;
this.settings = settings.ScreenProctoring; 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; this.text = text;
} }
@ -138,9 +140,9 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
logger.Info("Terminated proctoring."); 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) private void Connect(string sessionId = default)

View file

@ -52,9 +52,16 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring.Service
health = default; 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; return success;

View file

@ -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 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 timestamp = (Header.TIMESTAMP, DateTime.Now.ToUnixTimestamp().ToString());
var url = api.ScreenShotEndpoint.Replace(Api.SESSION_ID, sessionId); 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); var success = TryExecute(HttpMethod.Post, url, out var response, screenShot.Data, ContentType.OCTET_STREAM, Authorization, imageFormat, metdataJson, timestamp);

View file

@ -87,10 +87,10 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring.Service
return new ServiceResponse<int>(success, health, message); return new ServiceResponse<int>(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 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) if (success)
{ {

View file

@ -12,6 +12,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Timers; using System.Timers;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Proctoring.ScreenProctoring.Data; using SafeExamBrowser.Proctoring.ScreenProctoring.Data;
using SafeExamBrowser.Proctoring.ScreenProctoring.Imaging; using SafeExamBrowser.Proctoring.ScreenProctoring.Imaging;
@ -25,32 +26,34 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
const int BAD = 10; const int BAD = 10;
const int GOOD = 0; const int GOOD = 0;
private readonly Cache cache;
private readonly ILogger logger; 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 Random random;
private readonly ServiceProxy service; private readonly ServiceProxy service;
private readonly Timer timer; 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 int health;
private bool recovering; private bool recovering;
private DateTime resume; private DateTime resume;
private Thread thread; private Thread thread;
private CancellationTokenSource token; 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.logger = logger;
this.queue = new ConcurrentQueue<(Metadata, ScreenShot)>(); this.queue = new ConcurrentQueue<(MetaData, ScreenShot)>();
this.random = new Random(); this.random = new Random();
this.service = service; this.service = service;
this.timer = new Timer(); 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() internal void Start()
@ -68,9 +71,10 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
thread.IsBackground = true; thread.IsBackground = true;
thread.Start(); thread.Start();
// TODO: Only use timer when BAD, until then read health from transmission response!
timer.AutoReset = false; timer.AutoReset = false;
timer.Elapsed += Timer_Elapsed; timer.Elapsed += Timer_Elapsed;
timer.Interval = 2000; timer.Interval = 10000;
// TODO: Revert! // TODO: Revert!
// timer.Interval = FIFTEEN_SECONDS; // timer.Interval = FIFTEEN_SECONDS;
timer.Start(); timer.Start();
@ -93,17 +97,15 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
} }
catch (Exception e) catch (Exception e)
{ {
logger.Error("Failed to initiate cancellation!", e); logger.Error("Failed to initiate execution cancellation!", e);
} }
try try
{ {
var success = thread.Join(TEN_SECONDS); if (!thread.Join(TEN_SECONDS))
if (!success)
{ {
thread.Abort(); 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) catch (Exception e)
@ -125,7 +127,7 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
{ {
if (health == BAD) if (health == BAD)
{ {
ExecuteCacheOnly(); ExecuteCaching();
} }
else if (recovering) else if (recovering)
{ {
@ -146,7 +148,7 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
logger.Debug("Stopped."); logger.Debug("Stopped.");
} }
private void ExecuteCacheOnly() private void ExecuteCaching()
{ {
const int THREE_MINUTES = 180; 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})."); logger.Warn($"Activating local caching and suspending transmission due to bad service health (value: {health}, resume: {resume:HH:mm:ss}).");
} }
CacheBuffer(); CacheFromBuffer();
CacheFromQueue(); CacheFromQueue();
} }
private void ExecuteDeferred() private void ExecuteDeferred()
{ {
Schedule(health); BufferFromCache();
BufferFromQueue();
if (TryPeekFromBuffer(out _, out var schedule, out _) && schedule <= DateTime.Now) TryTransmitFromBuffer();
{
TryTransmitFromBuffer();
}
} }
private void ExecuteNormally() private void ExecuteNormally()
@ -181,19 +180,22 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
private void ExecuteRecovery() private void ExecuteRecovery()
{ {
CacheFromQueue();
recovering = DateTime.Now < resume; 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})."); 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.Enqueue((metaData, schedule, screenShot));
buffer = new Queue<(Metadata, DateTime, ScreenShot)>(buffer.OrderBy((b) => b.schedule)); buffer = new Queue<(MetaData, DateTime, ScreenShot)>(buffer.OrderBy((b) => b.schedule));
// TODO: Remove! // TODO: Remove!
PrintBuffer(); PrintBuffer();
@ -202,111 +204,117 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
private void PrintBuffer() private void PrintBuffer()
{ {
logger.Log("-------------------------------------------------------------------------------------------------------"); 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) 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("-------------------------------------------------------------------------------------------------------"); 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) Buffer(metaData, CalculateSchedule(metaData), screenShot);
{
Cache(metadata, screenShot);
}
}
// TODO: Revert!
// buffer.Clear();
}
private void CacheFromQueue()
{
if (TryDequeue(out var metadata, out var screenShot))
{
using (screenShot)
{
Cache(metadata, screenShot);
}
} }
} }
private void Cache(Metadata metadata, ScreenShot screenShot) private void BufferFromQueue()
{ {
// TODO: Implement caching! if (TryDequeue(out var metaData, out var screenShot))
//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))
{ {
var schedule = DateTime.Now.AddMilliseconds((health + 1) * metadata.Elapsed.TotalMilliseconds); Buffer(metaData, CalculateSchedule(metaData), screenShot);
Buffer(metadata, schedule, screenShot);
} }
} }
private bool TryDequeue(out Metadata metadata, out ScreenShot screenShot) private void CacheFromBuffer()
{ {
metadata = default; if (TryPeekFromBuffer(out var metaData, out _, out var screenShot))
screenShot = default;
if (queue.TryDequeue(out var item))
{ {
metadata = item.metadata; var success = cache.TryEnqueue(metaData, screenShot);
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);
if (success) if (success)
{ {
buffer.Dequeue(); buffer.Dequeue();
screenShot.Dispose(); 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(); PrintBuffer();
} }
} }
@ -316,17 +324,21 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
private bool TryTransmitFromCache() private bool TryTransmitFromCache()
{ {
var success = false; var success = true;
// TODO: Implement transmission from cache! if (cache.TryDequeue(out var metaData, out var screenShot))
//if (Cache.Any()) {
//{ success = TryTransmit(metaData, screenShot);
//
//} if (success)
//else {
//{ screenShot.Dispose();
// success = true; }
//} else
{
Buffer(metaData, DateTime.Now, screenShot);
}
}
return success; return success;
} }
@ -335,9 +347,9 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
{ {
var success = false; 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) if (success)
{ {
@ -345,20 +357,20 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
} }
else else
{ {
Buffer(metadata, DateTime.Now, screenShot); Buffer(metaData, DateTime.Now, screenShot);
} }
} }
return success; return success;
} }
private bool TryTransmit(Metadata metadata, ScreenShot screenShot) private bool TryTransmit(MetaData metaData, ScreenShot screenShot)
{ {
var success = false; var success = false;
if (service.IsConnected) if (service.IsConnected)
{ {
success = service.Send(metadata, screenShot).Success; success = service.Send(metaData, screenShot).Success;
} }
else else
{ {
@ -368,7 +380,8 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
return success; return success;
} }
private readonly Random temp = new Random(); private int factor = 2;
private int bads = 0;
private void Timer_Elapsed(object sender, ElapsedEventArgs e) private void Timer_Elapsed(object sender, ElapsedEventArgs e)
{ {
@ -396,12 +409,26 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring
var previous = health; var previous = health;
health += temp.Next(-3, 5); if (bads < 2)
health = health < GOOD ? GOOD : (health > BAD ? BAD : health); {
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) 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(); timer.Start();

View file

@ -14,6 +14,11 @@ namespace SafeExamBrowser.Server
{ {
internal static class Extensions 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) internal static string ToLogType(this LogLevel severity)
{ {
switch (severity) switch (severity)
@ -31,11 +36,6 @@ namespace SafeExamBrowser.Server
return "UNKNOWN"; return "UNKNOWN";
} }
internal static string ToLogString(this HttpResponseMessage response)
{
return $"{(int?) response?.StatusCode} {response?.StatusCode} {response?.ReasonPhrase}";
}
internal static long ToUnixTimestamp(this DateTime date) internal static long ToUnixTimestamp(this DateTime date)
{ {
return new DateTimeOffset(date).ToUnixTimeMilliseconds(); return new DateTimeOffset(date).ToUnixTimeMilliseconds();

View file

@ -30,7 +30,7 @@ namespace SafeExamBrowser.Server.Requests
var url = $"{api.ConfigurationEndpoint}?examId={exam.Id}"; var url = $"{api.ConfigurationEndpoint}?examId={exam.Id}";
var success = TryExecute(HttpMethod.Get, url, out var response, default, default, Authorization, Token); var success = TryExecute(HttpMethod.Get, url, out var response, default, default, Authorization, Token);
content = response.Content; content = response?.Content;
message = response.ToLogString(); message = response.ToLogString();
return success; return success;

View file

@ -36,7 +36,7 @@ namespace SafeExamBrowser.Server.Requests
var success = TryExecute(HttpMethod.Post, api.PingEndpoint, out var response, requestContent, ContentType.URL_ENCODED, Authorization, Token); 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(); message = response.ToLogString();
return success; return success;