SEBSP-23: Finished basic network redundancy. This build contains a service health simulation.
This commit is contained in:
parent
a213ec0f7d
commit
f902ee9598
18 changed files with 744 additions and 389 deletions
|
@ -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;
|
||||
|
|
|
@ -82,6 +82,8 @@
|
|||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="System.Xaml" />
|
||||
<Reference Include="System.Xml" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="WindowsBase" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
@ -91,14 +93,17 @@
|
|||
<Compile Include="ProctoringFactory.cs" />
|
||||
<Compile Include="ProctoringImplementation.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="ScreenProctoring\Cache.cs" />
|
||||
<Compile Include="ScreenProctoring\Data\IntervalTrigger.cs" />
|
||||
<Compile Include="ScreenProctoring\Data\KeyboardTrigger.cs" />
|
||||
<Compile Include="ScreenProctoring\Data\MetaData.cs" />
|
||||
<Compile Include="ScreenProctoring\Data\MouseTrigger.cs" />
|
||||
<Compile Include="ScreenProctoring\Events\DataCollectedEventHandler.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\ScreenShot.cs" />
|
||||
<Compile Include="ScreenProctoring\Imaging\ScreenShotProcessor.cs" />
|
||||
<Compile Include="ScreenProctoring\ScreenProctoringImplementation.cs" />
|
||||
<Compile Include="ScreenProctoring\Service\Api.cs" />
|
||||
<Compile Include="ScreenProctoring\DataCollector.cs" />
|
||||
|
|
242
SafeExamBrowser.Proctoring/ScreenProctoring/Cache.cs
Normal file
242
SafeExamBrowser.Proctoring/ScreenProctoring/Cache.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,6 +11,5 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring.Data
|
|||
internal class IntervalTrigger
|
||||
{
|
||||
public int ConfigurationValue { get; internal set; }
|
||||
public int TimeElapsed { get; internal set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()}";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -87,10 +87,10 @@ namespace SafeExamBrowser.Proctoring.ScreenProctoring.Service
|
|||
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 success = request.TryExecute(metadata, screenShot, SessionId, out var message);
|
||||
var success = request.TryExecute(metaData, screenShot, SessionId, out var message);
|
||||
|
||||
if (success)
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue