Restore SEBPatch
This commit is contained in:
211
SafeExamBrowser.Proctoring/ProctoringController.cs
Normal file
211
SafeExamBrowser.Proctoring/ProctoringController.cs
Normal file
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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 KGySoft.CoreLibraries;
|
||||
using SafeExamBrowser.Browser.Contracts;
|
||||
using SafeExamBrowser.Configuration.Contracts;
|
||||
using SafeExamBrowser.Core.Contracts.Notifications;
|
||||
using SafeExamBrowser.I18n.Contracts;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
using SafeExamBrowser.Monitoring.Contracts.Applications;
|
||||
using SafeExamBrowser.Proctoring.Contracts;
|
||||
using SafeExamBrowser.Proctoring.Contracts.Events;
|
||||
using SafeExamBrowser.Server.Contracts;
|
||||
using SafeExamBrowser.Server.Contracts.Events.Proctoring;
|
||||
using SafeExamBrowser.Settings.Proctoring;
|
||||
using SafeExamBrowser.SystemComponents.Contracts;
|
||||
using SafeExamBrowser.UserInterface.Contracts;
|
||||
using SafeExamBrowser.WindowsApi.Contracts;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring
|
||||
{
|
||||
public class ProctoringController : IProctoringController
|
||||
{
|
||||
private readonly ProctoringFactory factory;
|
||||
private readonly IModuleLogger logger;
|
||||
private readonly IServerProxy server;
|
||||
|
||||
private IEnumerable<ProctoringImplementation> implementations;
|
||||
|
||||
public bool IsHandRaised { get; private set; }
|
||||
public IEnumerable<INotification> Notifications => new List<INotification>(implementations);
|
||||
|
||||
public event ProctoringEventHandler HandLowered;
|
||||
public event ProctoringEventHandler HandRaised;
|
||||
public event RemainingWorkUpdatedEventHandler RemainingWorkUpdated
|
||||
{
|
||||
add { implementations.ForEach(i => i.RemainingWorkUpdated += value); }
|
||||
remove { implementations.ForEach(i => i.RemainingWorkUpdated -= value); }
|
||||
}
|
||||
|
||||
public ProctoringController(
|
||||
AppConfig appConfig,
|
||||
IApplicationMonitor applicationMonitor,
|
||||
IBrowserApplication browser,
|
||||
IFileSystem fileSystem,
|
||||
IModuleLogger logger,
|
||||
INativeMethods nativeMethods,
|
||||
IServerProxy server,
|
||||
IText text,
|
||||
IUserInterfaceFactory uiFactory)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.server = server;
|
||||
|
||||
factory = new ProctoringFactory(appConfig, applicationMonitor, browser, fileSystem, logger, nativeMethods, text, uiFactory);
|
||||
implementations = new List<ProctoringImplementation>();
|
||||
}
|
||||
|
||||
public void ExecuteRemainingWork()
|
||||
{
|
||||
foreach (var implementation in implementations)
|
||||
{
|
||||
try
|
||||
{
|
||||
implementation.ExecuteRemainingWork();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error($"Failed to execute remaining work for '{implementation.Name}'!", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasRemainingWork()
|
||||
{
|
||||
var hasWork = false;
|
||||
|
||||
foreach (var implementation in implementations)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (implementation.HasRemainingWork())
|
||||
{
|
||||
hasWork = true;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error($"Failed to check whether has remaining work for '{implementation.Name}'!", e);
|
||||
}
|
||||
}
|
||||
|
||||
return hasWork;
|
||||
}
|
||||
|
||||
public void Initialize(ProctoringSettings settings)
|
||||
{
|
||||
implementations = factory.CreateAllActive(settings);
|
||||
|
||||
server.HandConfirmed += Server_HandConfirmed;
|
||||
server.ProctoringConfigurationReceived += Server_ProctoringConfigurationReceived;
|
||||
server.ProctoringInstructionReceived += Server_ProctoringInstructionReceived;
|
||||
|
||||
foreach (var implementation in implementations)
|
||||
{
|
||||
try
|
||||
{
|
||||
implementation.Initialize();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error($"Failed to initialize proctoring implementation '{implementation.Name}'!", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void LowerHand()
|
||||
{
|
||||
var response = server.LowerHand();
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
IsHandRaised = false;
|
||||
HandLowered?.Invoke();
|
||||
|
||||
logger.Info("Hand lowered.");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error($"Failed to send lower hand notification to server! Message: {response.Message}.");
|
||||
}
|
||||
}
|
||||
|
||||
public void RaiseHand(string message = null)
|
||||
{
|
||||
var response = server.RaiseHand(message);
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
IsHandRaised = true;
|
||||
HandRaised?.Invoke();
|
||||
|
||||
logger.Info("Hand raised.");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error($"Failed to send raise hand notification to server! Message: {response.Message}.");
|
||||
}
|
||||
}
|
||||
|
||||
public void Terminate()
|
||||
{
|
||||
foreach (var implementation in implementations)
|
||||
{
|
||||
try
|
||||
{
|
||||
implementation.Terminate();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error($"Failed to terminate proctoring implementation '{implementation.Name}'!", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Server_HandConfirmed()
|
||||
{
|
||||
logger.Info("Hand confirmation received.");
|
||||
|
||||
IsHandRaised = false;
|
||||
HandLowered?.Invoke();
|
||||
}
|
||||
|
||||
private void Server_ProctoringConfigurationReceived(bool allowChat, bool receiveAudio, bool receiveVideo)
|
||||
{
|
||||
foreach (var implementation in implementations)
|
||||
{
|
||||
try
|
||||
{
|
||||
implementation.ProctoringConfigurationReceived(allowChat, receiveAudio, receiveVideo);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error($"Failed to update proctoring configuration for '{implementation.Name}'!", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Server_ProctoringInstructionReceived(InstructionEventArgs args)
|
||||
{
|
||||
foreach (var implementation in implementations)
|
||||
{
|
||||
try
|
||||
{
|
||||
implementation.ProctoringInstructionReceived(args);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error($"Failed to process proctoring instruction for '{implementation.Name}'!", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
70
SafeExamBrowser.Proctoring/ProctoringFactory.cs
Normal file
70
SafeExamBrowser.Proctoring/ProctoringFactory.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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.Collections.Generic;
|
||||
using SafeExamBrowser.Browser.Contracts;
|
||||
using SafeExamBrowser.Configuration.Contracts;
|
||||
using SafeExamBrowser.I18n.Contracts;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
using SafeExamBrowser.Monitoring.Contracts.Applications;
|
||||
using SafeExamBrowser.Proctoring.ScreenProctoring;
|
||||
using SafeExamBrowser.Proctoring.ScreenProctoring.Service;
|
||||
using SafeExamBrowser.Settings.Proctoring;
|
||||
using SafeExamBrowser.SystemComponents.Contracts;
|
||||
using SafeExamBrowser.UserInterface.Contracts;
|
||||
using SafeExamBrowser.WindowsApi.Contracts;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring
|
||||
{
|
||||
internal class ProctoringFactory
|
||||
{
|
||||
private readonly AppConfig appConfig;
|
||||
private readonly IApplicationMonitor applicationMonitor;
|
||||
private readonly IBrowserApplication browser;
|
||||
private readonly IFileSystem fileSystem;
|
||||
private readonly IModuleLogger logger;
|
||||
private readonly INativeMethods nativeMethods;
|
||||
private readonly IText text;
|
||||
private readonly IUserInterfaceFactory uiFactory;
|
||||
|
||||
public ProctoringFactory(
|
||||
AppConfig appConfig,
|
||||
IApplicationMonitor applicationMonitor,
|
||||
IBrowserApplication browser,
|
||||
IFileSystem fileSystem,
|
||||
IModuleLogger logger,
|
||||
INativeMethods nativeMethods,
|
||||
IText text,
|
||||
IUserInterfaceFactory uiFactory)
|
||||
{
|
||||
this.appConfig = appConfig;
|
||||
this.applicationMonitor = applicationMonitor;
|
||||
this.browser = browser;
|
||||
this.fileSystem = fileSystem;
|
||||
this.logger = logger;
|
||||
this.nativeMethods = nativeMethods;
|
||||
this.text = text;
|
||||
this.uiFactory = uiFactory;
|
||||
}
|
||||
|
||||
internal IEnumerable<ProctoringImplementation> CreateAllActive(ProctoringSettings settings)
|
||||
{
|
||||
var implementations = new List<ProctoringImplementation>();
|
||||
|
||||
if (settings.ScreenProctoring.Enabled)
|
||||
{
|
||||
var logger = this.logger.CloneFor(nameof(ScreenProctoring));
|
||||
var service = new ServiceProxy(logger.CloneFor(nameof(ServiceProxy)));
|
||||
|
||||
implementations.Add(new ScreenProctoringImplementation(appConfig, applicationMonitor, browser, logger, nativeMethods, service, settings, text));
|
||||
}
|
||||
|
||||
return implementations;
|
||||
}
|
||||
}
|
||||
}
|
61
SafeExamBrowser.Proctoring/ProctoringImplementation.cs
Normal file
61
SafeExamBrowser.Proctoring/ProctoringImplementation.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (c) 2022 ETH Zürich, IT Services
|
||||
*
|
||||
* 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 SafeExamBrowser.Core.Contracts.Notifications;
|
||||
using SafeExamBrowser.Core.Contracts.Notifications.Events;
|
||||
using SafeExamBrowser.Core.Contracts.Resources.Icons;
|
||||
using SafeExamBrowser.Proctoring.Contracts.Events;
|
||||
using SafeExamBrowser.Server.Contracts.Events.Proctoring;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring
|
||||
{
|
||||
internal abstract class ProctoringImplementation : INotification
|
||||
{
|
||||
internal abstract string Name { get; }
|
||||
|
||||
public bool CanActivate { get; protected set; }
|
||||
public string Tooltip { get; protected set; }
|
||||
public IconResource IconResource { get; protected set; }
|
||||
|
||||
internal event RemainingWorkUpdatedEventHandler RemainingWorkUpdated;
|
||||
public event NotificationChangedEventHandler NotificationChanged;
|
||||
|
||||
void INotification.Activate()
|
||||
{
|
||||
ActivateNotification();
|
||||
}
|
||||
|
||||
void INotification.Terminate()
|
||||
{
|
||||
TerminateNotification();
|
||||
}
|
||||
|
||||
internal abstract void Initialize();
|
||||
internal abstract void ProctoringConfigurationReceived(bool allowChat, bool receiveAudio, bool receiveVideo);
|
||||
internal abstract void ProctoringInstructionReceived(InstructionEventArgs args);
|
||||
internal abstract void Start();
|
||||
internal abstract void Stop();
|
||||
internal abstract void Terminate();
|
||||
|
||||
internal virtual void ExecuteRemainingWork() { }
|
||||
internal virtual bool HasRemainingWork() => false;
|
||||
|
||||
protected virtual void ActivateNotification() { }
|
||||
protected virtual void TerminateNotification() { }
|
||||
|
||||
protected void InvokeNotificationChanged()
|
||||
{
|
||||
NotificationChanged?.Invoke();
|
||||
}
|
||||
|
||||
protected void InvokeRemainingWorkUpdated(RemainingWorkUpdatedEventArgs args)
|
||||
{
|
||||
RemainingWorkUpdated?.Invoke(args);
|
||||
}
|
||||
}
|
||||
}
|
32
SafeExamBrowser.Proctoring/Properties/AssemblyInfo.cs
Normal file
32
SafeExamBrowser.Proctoring/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("SafeExamBrowser.Proctoring")]
|
||||
[assembly: AssemblyDescription("Safe Exam Browser")]
|
||||
[assembly: AssemblyCompany("ETH Zürich")]
|
||||
[assembly: AssemblyProduct("SafeExamBrowser.Proctoring")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2024 ETH Zürich, IT Services")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("3f1f262e-a07c-4513-83c6-d7ef2f203ebf")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
[assembly: AssemblyInformationalVersion("1.0.0.0")]
|
173
SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj
Normal file
173
SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj
Normal file
@@ -0,0 +1,173 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{3F1F262E-A07C-4513-83C6-D7EF2F203EBF}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>SafeExamBrowser.Proctoring</RootNamespace>
|
||||
<AssemblyName>SafeExamBrowser.Proctoring</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<Deterministic>true</Deterministic>
|
||||
<NuGetPackageImportStamp>
|
||||
</NuGetPackageImportStamp>
|
||||
<TargetFrameworkProfile />
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<OutputPath>bin\x64\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<DebugType>full</DebugType>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
<OutputPath>bin\x64\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<Optimize>true</Optimize>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<OutputPath>bin\x86\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<DebugType>full</DebugType>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
|
||||
<OutputPath>bin\x86\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<Optimize>true</Optimize>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="KGySoft.CoreLibraries, Version=8.1.0.0, Culture=neutral, PublicKeyToken=b45eba277439ddfe, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\KGySoft.CoreLibraries.8.1.0\lib\net472\KGySoft.CoreLibraries.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="KGySoft.Drawing, Version=8.1.0.0, Culture=neutral, PublicKeyToken=b45eba277439ddfe, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\KGySoft.Drawing.8.1.0\lib\net46\KGySoft.Drawing.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="KGySoft.Drawing.Core, Version=8.1.0.0, Culture=neutral, PublicKeyToken=b45eba277439ddfe, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\KGySoft.Drawing.Core.8.1.0\lib\net46\KGySoft.Drawing.Core.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="PresentationCore" />
|
||||
<Reference Include="PresentationFramework" />
|
||||
<Reference Include="System" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<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>
|
||||
<Compile Include="ProctoringController.cs" />
|
||||
<Compile Include="ProctoringFactory.cs" />
|
||||
<Compile Include="ProctoringImplementation.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="ScreenProctoring\Buffer.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\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" />
|
||||
<Compile Include="ScreenProctoring\Service\Parser.cs" />
|
||||
<Compile Include="ScreenProctoring\Service\Requests\ContentType.cs" />
|
||||
<Compile Include="ScreenProctoring\Service\Requests\CreateSessionRequest.cs" />
|
||||
<Compile Include="ScreenProctoring\Service\Requests\Header.cs" />
|
||||
<Compile Include="ScreenProctoring\Service\Requests\Extensions.cs" />
|
||||
<Compile Include="ScreenProctoring\Service\Requests\HealthRequest.cs" />
|
||||
<Compile Include="ScreenProctoring\Service\Requests\TerminateSessionRequest.cs" />
|
||||
<Compile Include="ScreenProctoring\Service\Requests\OAuth2TokenRequest.cs" />
|
||||
<Compile Include="ScreenProctoring\Service\Requests\Request.cs" />
|
||||
<Compile Include="ScreenProctoring\Service\Requests\ScreenShotRequest.cs" />
|
||||
<Compile Include="ScreenProctoring\Service\ServiceProxy.cs" />
|
||||
<Compile Include="ScreenProctoring\Service\ServiceResponse.cs" />
|
||||
<Compile Include="ScreenProctoring\TransmissionSpooler.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SafeExamBrowser.Applications.Contracts\SafeExamBrowser.Applications.Contracts.csproj">
|
||||
<Project>{ac77745d-3b41-43e2-8e84-d40e5a4ee77f}</Project>
|
||||
<Name>SafeExamBrowser.Applications.Contracts</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\SafeExamBrowser.Browser.Contracts\SafeExamBrowser.Browser.Contracts.csproj">
|
||||
<Project>{5FB5273D-277C-41DD-8593-A25CE1AFF2E9}</Project>
|
||||
<Name>SafeExamBrowser.Browser.Contracts</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\SafeExamBrowser.Configuration.Contracts\SafeExamBrowser.Configuration.Contracts.csproj">
|
||||
<Project>{7d74555e-63e1-4c46-bd0a-8580552368c8}</Project>
|
||||
<Name>SafeExamBrowser.Configuration.Contracts</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\SafeExamBrowser.Core.Contracts\SafeExamBrowser.Core.Contracts.csproj">
|
||||
<Project>{fe0e1224-b447-4b14-81e7-ed7d84822aa0}</Project>
|
||||
<Name>SafeExamBrowser.Core.Contracts</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\SafeExamBrowser.I18n.Contracts\SafeExamBrowser.I18n.Contracts.csproj">
|
||||
<Project>{1858ddf3-bc2a-4bff-b663-4ce2ffeb8b7d}</Project>
|
||||
<Name>SafeExamBrowser.I18n.Contracts</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\SafeExamBrowser.Logging.Contracts\SafeExamBrowser.Logging.Contracts.csproj">
|
||||
<Project>{64ea30fb-11d4-436a-9c2b-88566285363e}</Project>
|
||||
<Name>SafeExamBrowser.Logging.Contracts</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\SafeExamBrowser.Monitoring.Contracts\SafeExamBrowser.Monitoring.Contracts.csproj">
|
||||
<Project>{6D563A30-366D-4C35-815B-2C9E6872278B}</Project>
|
||||
<Name>SafeExamBrowser.Monitoring.Contracts</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\SafeExamBrowser.Proctoring.Contracts\SafeExamBrowser.Proctoring.Contracts.csproj">
|
||||
<Project>{8e52bd1c-0540-4f16-b181-6665d43f7a7b}</Project>
|
||||
<Name>SafeExamBrowser.Proctoring.Contracts</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\SafeExamBrowser.Server.Contracts\SafeExamBrowser.Server.Contracts.csproj">
|
||||
<Project>{db701e6f-bddc-4cec-b662-335a9dc11809}</Project>
|
||||
<Name>SafeExamBrowser.Server.Contracts</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\SafeExamBrowser.Settings\SafeExamBrowser.Settings.csproj">
|
||||
<Project>{30b2d907-5861-4f39-abad-c4abf1b3470e}</Project>
|
||||
<Name>SafeExamBrowser.Settings</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\SafeExamBrowser.SystemComponents.Contracts\SafeExamBrowser.SystemComponents.Contracts.csproj">
|
||||
<Project>{903129c6-e236-493b-9ad6-c6a57f647a3a}</Project>
|
||||
<Name>SafeExamBrowser.SystemComponents.Contracts</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\SafeExamBrowser.UserInterface.Contracts\SafeExamBrowser.UserInterface.Contracts.csproj">
|
||||
<Project>{c7889e97-6ff6-4a58-b7cb-521ed276b316}</Project>
|
||||
<Name>SafeExamBrowser.UserInterface.Contracts</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\SafeExamBrowser.WindowsApi.Contracts\SafeExamBrowser.WindowsApi.Contracts.csproj">
|
||||
<Project>{7016F080-9AA5-41B2-A225-385AD877C171}</Project>
|
||||
<Name>SafeExamBrowser.WindowsApi.Contracts</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup />
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
92
SafeExamBrowser.Proctoring/ScreenProctoring/Buffer.cs
Normal file
92
SafeExamBrowser.Proctoring/ScreenProctoring/Buffer.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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.Linq;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
using SafeExamBrowser.Proctoring.ScreenProctoring.Data;
|
||||
using SafeExamBrowser.Proctoring.ScreenProctoring.Imaging;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring
|
||||
{
|
||||
internal class Buffer
|
||||
{
|
||||
private readonly object @lock = new object();
|
||||
|
||||
private readonly List<(MetaData metaData, DateTime schedule, ScreenShot screenShot)> list;
|
||||
private readonly ILogger logger;
|
||||
|
||||
internal int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (@lock)
|
||||
{
|
||||
return list.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal Buffer(ILogger logger)
|
||||
{
|
||||
this.list = new List<(MetaData, DateTime, ScreenShot)>();
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
internal bool Any()
|
||||
{
|
||||
lock (@lock)
|
||||
{
|
||||
return list.Any();
|
||||
}
|
||||
}
|
||||
|
||||
internal void Dequeue()
|
||||
{
|
||||
lock (@lock)
|
||||
{
|
||||
if (list.Any())
|
||||
{
|
||||
var (_, schedule, screenShot) = list.First();
|
||||
|
||||
list.RemoveAt(0);
|
||||
logger.Debug($"Removed data for '{screenShot.CaptureTime:HH:mm:ss} -> {schedule:HH:mm:ss}', {Count} item(s) remaining.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void Enqueue(MetaData metaData, DateTime schedule, ScreenShot screenShot)
|
||||
{
|
||||
lock (@lock)
|
||||
{
|
||||
list.Add((metaData, schedule, screenShot));
|
||||
list.Sort((a, b) => DateTime.Compare(a.schedule, b.schedule));
|
||||
|
||||
logger.Debug($"Buffered data for '{screenShot.CaptureTime:HH:mm:ss} -> {schedule:HH:mm:ss}', now holding {Count} item(s).");
|
||||
}
|
||||
}
|
||||
|
||||
internal bool TryPeek(out MetaData metaData, out DateTime schedule, out ScreenShot screenShot)
|
||||
{
|
||||
lock (@lock)
|
||||
{
|
||||
metaData = default;
|
||||
schedule = default;
|
||||
screenShot = default;
|
||||
|
||||
if (list.Any())
|
||||
{
|
||||
(metaData, schedule, screenShot) = list.First();
|
||||
}
|
||||
|
||||
return metaData != default && screenShot != default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
241
SafeExamBrowser.Proctoring/ScreenProctoring/Cache.cs
Normal file
241
SafeExamBrowser.Proctoring/ScreenProctoring/Cache.cs
Normal file
@@ -0,0 +1,241 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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.Concurrent;
|
||||
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 ConcurrentQueue<(string fileName, int checksum, string hash)> queue;
|
||||
|
||||
internal int Count => queue.Count;
|
||||
internal string Directory { get; private set; }
|
||||
|
||||
public Cache(AppConfig appConfig, ILogger logger)
|
||||
{
|
||||
this.appConfig = appConfig;
|
||||
this.logger = logger;
|
||||
this.queue = new ConcurrentQueue<(string, int, string)>();
|
||||
}
|
||||
|
||||
internal bool Any()
|
||||
{
|
||||
return queue.Any();
|
||||
}
|
||||
|
||||
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}', now holding {Count} item(s).");
|
||||
}
|
||||
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() && queue.TryPeek(out var item))
|
||||
{
|
||||
try
|
||||
{
|
||||
LoadData(item.fileName, out metaData, out screenShot);
|
||||
LoadImage(item.fileName, screenShot);
|
||||
Dequeue(item.fileName, item.checksum, item.hash, metaData, screenShot);
|
||||
|
||||
success = true;
|
||||
|
||||
logger.Debug($"Removed data for '{item.fileName}', {Count} item(s) remaining.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error($"Failed to remove data for '{item.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);
|
||||
|
||||
while (!queue.TryDequeue(out _)) ;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Data
|
||||
{
|
||||
internal class IntervalTrigger
|
||||
{
|
||||
public int ConfigurationValue { get; internal set; }
|
||||
}
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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.Windows.Input;
|
||||
using SafeExamBrowser.WindowsApi.Contracts.Events;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Data
|
||||
{
|
||||
internal class KeyboardTrigger
|
||||
{
|
||||
public Key Key { get; internal set; }
|
||||
public KeyModifier Modifier { get; internal set; }
|
||||
public KeyState State { get; internal set; }
|
||||
}
|
||||
}
|
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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.Settings.Proctoring;
|
||||
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 readonly MetaDataSettings settings;
|
||||
|
||||
private string applicationInfo;
|
||||
private string browserInfo;
|
||||
private TimeSpan elapsed;
|
||||
private string triggerInfo;
|
||||
private string urls;
|
||||
private int urlCount;
|
||||
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,
|
||||
MetaDataSettings settings)
|
||||
{
|
||||
this.applicationMonitor = applicationMonitor;
|
||||
this.browser = browser;
|
||||
this.elapsed = elapsed;
|
||||
this.logger = logger;
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
internal void Capture(IntervalTrigger interval = default, KeyboardTrigger keyboard = default, MouseTrigger mouse = default)
|
||||
{
|
||||
Initialize();
|
||||
CaptureApplicationData();
|
||||
CaptureBrowserData();
|
||||
|
||||
if (interval != default)
|
||||
{
|
||||
CaptureIntervalTrigger(interval);
|
||||
}
|
||||
else if (keyboard != default)
|
||||
{
|
||||
CaptureKeyboardTrigger(keyboard);
|
||||
}
|
||||
else if (mouse != default)
|
||||
{
|
||||
CaptureMouseTrigger(mouse);
|
||||
}
|
||||
|
||||
logger.Debug($"Captured metadata: {applicationInfo} / {browserInfo} / {urlCount} URL(s) / {triggerInfo} / {windowTitle}.");
|
||||
}
|
||||
|
||||
private void CaptureApplicationData()
|
||||
{
|
||||
if (applicationMonitor.TryGetActiveApplication(out var application))
|
||||
{
|
||||
if (settings.CaptureApplicationData)
|
||||
{
|
||||
applicationInfo = BuildApplicationInfo(application);
|
||||
}
|
||||
|
||||
if (settings.CaptureWindowTitle)
|
||||
{
|
||||
windowTitle = string.IsNullOrEmpty(application.Window.Title) ? "-" : application.Window.Title;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CaptureBrowserData()
|
||||
{
|
||||
if (settings.CaptureBrowserData)
|
||||
{
|
||||
var windows = browser.GetWindows();
|
||||
|
||||
browserInfo = string.Join(", ", windows.Select(w => $"{(w.IsMainWindow ? "Main" : "Additional")} Window: {w.Title}"));
|
||||
urls = string.Join(", ", windows.Select(w => w.Url));
|
||||
urlCount = windows.Count();
|
||||
}
|
||||
}
|
||||
|
||||
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) && !keyboard.Key.ToString().Contains(m.ToString()));
|
||||
var modifiers = flags.Any() ? string.Join(" + ", flags) + " + " : string.Empty;
|
||||
|
||||
if (flags.Any())
|
||||
{
|
||||
triggerInfo = $"'{modifiers}{keyboard.Key}' has been {keyboard.State.ToString().ToLower()}.";
|
||||
}
|
||||
else
|
||||
{
|
||||
triggerInfo = $"A 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();
|
||||
}
|
||||
|
||||
private void Initialize()
|
||||
{
|
||||
applicationInfo = "-";
|
||||
browserInfo = "-";
|
||||
triggerInfo = "-";
|
||||
urls = "-";
|
||||
windowTitle = "-";
|
||||
}
|
||||
}
|
||||
}
|
39
SafeExamBrowser.Proctoring/ScreenProctoring/Data/Metadata.cs
Normal file
39
SafeExamBrowser.Proctoring/ScreenProctoring/Data/Metadata.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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 Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using SafeExamBrowser.Proctoring.ScreenProctoring.Service.Requests;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Data
|
||||
{
|
||||
internal class MetaData
|
||||
{
|
||||
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()
|
||||
{
|
||||
var json = new JObject
|
||||
{
|
||||
[Header.Metadata.ApplicationInfo] = ApplicationInfo,
|
||||
[Header.Metadata.BrowserInfo] = BrowserInfo,
|
||||
[Header.Metadata.BrowserUrls] = Urls,
|
||||
[Header.Metadata.TriggerInfo] = TriggerInfo,
|
||||
[Header.Metadata.WindowTitle] = WindowTitle
|
||||
};
|
||||
|
||||
return json.ToString(Formatting.None);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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 SafeExamBrowser.WindowsApi.Contracts.Events;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Data
|
||||
{
|
||||
internal class MouseTrigger
|
||||
{
|
||||
public MouseButton Button { get; internal set; }
|
||||
public MouseInformation Info { get; internal set; }
|
||||
public MouseButtonState State { get; internal set; }
|
||||
}
|
||||
}
|
177
SafeExamBrowser.Proctoring/ScreenProctoring/DataCollector.cs
Normal file
177
SafeExamBrowser.Proctoring/ScreenProctoring/DataCollector.cs
Normal file
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Timers;
|
||||
using System.Windows.Input;
|
||||
using SafeExamBrowser.Browser.Contracts;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
using SafeExamBrowser.Monitoring.Contracts.Applications;
|
||||
using SafeExamBrowser.Proctoring.ScreenProctoring.Data;
|
||||
using SafeExamBrowser.Proctoring.ScreenProctoring.Events;
|
||||
using SafeExamBrowser.Proctoring.ScreenProctoring.Imaging;
|
||||
using SafeExamBrowser.Settings.Proctoring;
|
||||
using SafeExamBrowser.WindowsApi.Contracts;
|
||||
using SafeExamBrowser.WindowsApi.Contracts.Events;
|
||||
using MouseButton = SafeExamBrowser.WindowsApi.Contracts.Events.MouseButton;
|
||||
using MouseButtonState = SafeExamBrowser.WindowsApi.Contracts.Events.MouseButtonState;
|
||||
using Timer = System.Timers.Timer;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring
|
||||
{
|
||||
internal class DataCollector
|
||||
{
|
||||
private readonly object @lock = new object();
|
||||
|
||||
private readonly IApplicationMonitor applicationMonitor;
|
||||
private readonly IBrowserApplication browser;
|
||||
private readonly IModuleLogger logger;
|
||||
private readonly INativeMethods nativeMethods;
|
||||
private readonly ScreenProctoringSettings settings;
|
||||
private readonly Timer timer;
|
||||
|
||||
private DateTime last;
|
||||
private Guid? keyboardHookId;
|
||||
private Guid? mouseHookId;
|
||||
|
||||
internal event DataCollectedEventHandler DataCollected;
|
||||
|
||||
internal DataCollector(
|
||||
IApplicationMonitor applicationMonitor,
|
||||
IBrowserApplication browser,
|
||||
IModuleLogger logger,
|
||||
INativeMethods nativeMethods,
|
||||
ScreenProctoringSettings settings)
|
||||
{
|
||||
this.applicationMonitor = applicationMonitor;
|
||||
this.browser = browser;
|
||||
this.logger = logger;
|
||||
this.nativeMethods = nativeMethods;
|
||||
this.settings = settings;
|
||||
this.timer = new Timer();
|
||||
}
|
||||
|
||||
internal void Start()
|
||||
{
|
||||
last = DateTime.Now;
|
||||
|
||||
keyboardHookId = nativeMethods.RegisterKeyboardHook(KeyboardHookCallback);
|
||||
mouseHookId = nativeMethods.RegisterMouseHook(MouseHookCallback);
|
||||
|
||||
timer.AutoReset = false;
|
||||
timer.Elapsed += MaxIntervalElapsed;
|
||||
timer.Interval = settings.MaxInterval;
|
||||
timer.Start();
|
||||
|
||||
logger.Debug("Started.");
|
||||
}
|
||||
|
||||
internal void Stop()
|
||||
{
|
||||
last = DateTime.Now;
|
||||
|
||||
if (keyboardHookId.HasValue)
|
||||
{
|
||||
nativeMethods.DeregisterKeyboardHook(keyboardHookId.Value);
|
||||
}
|
||||
|
||||
if (mouseHookId.HasValue)
|
||||
{
|
||||
nativeMethods.DeregisterMouseHook(mouseHookId.Value);
|
||||
}
|
||||
|
||||
keyboardHookId = default;
|
||||
mouseHookId = default;
|
||||
|
||||
timer.Elapsed -= MaxIntervalElapsed;
|
||||
timer.Stop();
|
||||
|
||||
logger.Debug("Stopped.");
|
||||
}
|
||||
|
||||
private bool KeyboardHookCallback(int keyCode, KeyModifier modifier, KeyState state)
|
||||
{
|
||||
var trigger = new KeyboardTrigger
|
||||
{
|
||||
Key = KeyInterop.KeyFromVirtualKey(keyCode),
|
||||
Modifier = modifier,
|
||||
State = state
|
||||
};
|
||||
|
||||
TryCollect(keyboard: trigger);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void MaxIntervalElapsed(object sender, ElapsedEventArgs args)
|
||||
{
|
||||
var trigger = new IntervalTrigger
|
||||
{
|
||||
ConfigurationValue = settings.MaxInterval,
|
||||
};
|
||||
|
||||
TryCollect(interval: trigger);
|
||||
}
|
||||
|
||||
private bool MouseHookCallback(MouseButton button, MouseButtonState state, MouseInformation info)
|
||||
{
|
||||
var trigger = new MouseTrigger
|
||||
{
|
||||
Button = button,
|
||||
Info = info,
|
||||
State = state
|
||||
};
|
||||
|
||||
TryCollect(mouse: trigger);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void TryCollect(IntervalTrigger interval = default, KeyboardTrigger keyboard = default, MouseTrigger mouse = default)
|
||||
{
|
||||
if (MinIntervalElapsed() && Monitor.TryEnter(@lock))
|
||||
{
|
||||
var elapsed = DateTime.Now.Subtract(last);
|
||||
|
||||
last = DateTime.Now;
|
||||
timer.Stop();
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var metaData = new MetaDataAggregator(applicationMonitor, browser, elapsed, logger.CloneFor(nameof(MetaDataAggregator)), settings.MetaData);
|
||||
var screenShot = new ScreenShotProcessor(logger.CloneFor(nameof(ScreenShotProcessor)), settings);
|
||||
|
||||
metaData.Capture(interval, keyboard, mouse);
|
||||
screenShot.Take();
|
||||
screenShot.Compress();
|
||||
|
||||
DataCollected?.Invoke(metaData.Data, screenShot.Data);
|
||||
|
||||
screenShot.Dispose();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error("Failed to execute data collection!", e);
|
||||
}
|
||||
});
|
||||
|
||||
timer.Start();
|
||||
Monitor.Exit(@lock);
|
||||
}
|
||||
}
|
||||
|
||||
private bool MinIntervalElapsed()
|
||||
{
|
||||
return DateTime.Now.Subtract(last) >= new TimeSpan(0, 0, 0, 0, settings.MinInterval);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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 SafeExamBrowser.Proctoring.ScreenProctoring.Data;
|
||||
using SafeExamBrowser.Proctoring.ScreenProctoring.Imaging;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Events
|
||||
{
|
||||
internal delegate void DataCollectedEventHandler(MetaData metaData, ScreenShot screenShot);
|
||||
}
|
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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.Windows.Forms;
|
||||
using KGySoft.Drawing.Imaging;
|
||||
using SafeExamBrowser.Settings.Proctoring;
|
||||
using ImageFormat = SafeExamBrowser.Settings.Proctoring.ImageFormat;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Imaging
|
||||
{
|
||||
internal static class Extensions
|
||||
{
|
||||
internal static void DrawCursorPosition(this Graphics graphics)
|
||||
{
|
||||
graphics.DrawArc(new Pen(Color.Red, 3), Cursor.Position.X - 25, Cursor.Position.Y - 25, 50, 50, 0, 360);
|
||||
graphics.DrawArc(new Pen(Color.Yellow, 3), Cursor.Position.X - 22, Cursor.Position.Y - 22, 44, 44, 0, 360);
|
||||
graphics.FillEllipse(Brushes.Red, Cursor.Position.X - 4, Cursor.Position.Y - 4, 8, 8);
|
||||
graphics.FillEllipse(Brushes.Yellow, Cursor.Position.X - 2, Cursor.Position.Y - 2, 4, 4);
|
||||
}
|
||||
|
||||
internal static PixelFormat ToPixelFormat(this ImageQuantization quantization)
|
||||
{
|
||||
switch (quantization)
|
||||
{
|
||||
case ImageQuantization.BlackAndWhite1bpp:
|
||||
return PixelFormat.Format1bppIndexed;
|
||||
case ImageQuantization.Color8bpp:
|
||||
return PixelFormat.Format8bppIndexed;
|
||||
case ImageQuantization.Color16bpp:
|
||||
return PixelFormat.Format16bppArgb1555;
|
||||
case ImageQuantization.Color24bpp:
|
||||
return PixelFormat.Format24bppRgb;
|
||||
case ImageQuantization.Grayscale2bpp:
|
||||
return PixelFormat.Format4bppIndexed;
|
||||
case ImageQuantization.Grayscale4bpp:
|
||||
return PixelFormat.Format4bppIndexed;
|
||||
case ImageQuantization.Grayscale8bpp:
|
||||
return PixelFormat.Format8bppIndexed;
|
||||
default:
|
||||
throw new NotImplementedException($"Image quantization '{quantization}' is not yet implemented!");
|
||||
}
|
||||
}
|
||||
|
||||
internal static IQuantizer ToQuantizer(this ImageQuantization quantization)
|
||||
{
|
||||
switch (quantization)
|
||||
{
|
||||
case ImageQuantization.BlackAndWhite1bpp:
|
||||
return PredefinedColorsQuantizer.BlackAndWhite();
|
||||
case ImageQuantization.Color8bpp:
|
||||
return PredefinedColorsQuantizer.SystemDefault8BppPalette();
|
||||
case ImageQuantization.Color16bpp:
|
||||
return PredefinedColorsQuantizer.Rgb555();
|
||||
case ImageQuantization.Color24bpp:
|
||||
return PredefinedColorsQuantizer.Rgb888();
|
||||
case ImageQuantization.Grayscale2bpp:
|
||||
return PredefinedColorsQuantizer.Grayscale4();
|
||||
case ImageQuantization.Grayscale4bpp:
|
||||
return PredefinedColorsQuantizer.Grayscale16();
|
||||
case ImageQuantization.Grayscale8bpp:
|
||||
return PredefinedColorsQuantizer.Grayscale();
|
||||
default:
|
||||
throw new NotImplementedException($"Image quantization '{quantization}' is not yet implemented!");
|
||||
}
|
||||
}
|
||||
|
||||
internal static System.Drawing.Imaging.ImageFormat ToSystemFormat(this ImageFormat format)
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case ImageFormat.Bmp:
|
||||
return System.Drawing.Imaging.ImageFormat.Bmp;
|
||||
case ImageFormat.Gif:
|
||||
return System.Drawing.Imaging.ImageFormat.Gif;
|
||||
case ImageFormat.Jpg:
|
||||
return System.Drawing.Imaging.ImageFormat.Jpeg;
|
||||
case ImageFormat.Png:
|
||||
return System.Drawing.Imaging.ImageFormat.Png;
|
||||
default:
|
||||
throw new NotImplementedException($"Image format '{format}' is not yet implemented!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Imaging
|
||||
{
|
||||
internal enum ProcessingOrder
|
||||
{
|
||||
DownscalingQuantizing,
|
||||
QuantizingDownscaling,
|
||||
}
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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 SafeExamBrowser.Settings.Proctoring;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Imaging
|
||||
{
|
||||
internal class ScreenShot : IDisposable
|
||||
{
|
||||
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()
|
||||
{
|
||||
Data = default;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"captured: {CaptureTime}, format: {Format.ToString().ToUpper()}, resolution: {Width}x{Height}, size: {Data.Length / 1000:N0}kB";
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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()}";
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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 SafeExamBrowser.Browser.Contracts;
|
||||
using SafeExamBrowser.Configuration.Contracts;
|
||||
using SafeExamBrowser.Core.Contracts.Resources.Icons;
|
||||
using SafeExamBrowser.I18n.Contracts;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
using SafeExamBrowser.Monitoring.Contracts.Applications;
|
||||
using SafeExamBrowser.Proctoring.ScreenProctoring.Data;
|
||||
using SafeExamBrowser.Proctoring.ScreenProctoring.Imaging;
|
||||
using SafeExamBrowser.Proctoring.ScreenProctoring.Service;
|
||||
using SafeExamBrowser.Server.Contracts.Events.Proctoring;
|
||||
using SafeExamBrowser.Settings.Proctoring;
|
||||
using SafeExamBrowser.WindowsApi.Contracts;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring
|
||||
{
|
||||
internal class ScreenProctoringImplementation : ProctoringImplementation
|
||||
{
|
||||
private readonly DataCollector collector;
|
||||
private readonly IModuleLogger logger;
|
||||
private readonly ServiceProxy service;
|
||||
private readonly ScreenProctoringSettings settings;
|
||||
private readonly TransmissionSpooler spooler;
|
||||
private readonly IText text;
|
||||
|
||||
internal override string Name => nameof(ScreenProctoring);
|
||||
|
||||
internal ScreenProctoringImplementation(
|
||||
AppConfig appConfig,
|
||||
IApplicationMonitor applicationMonitor,
|
||||
IBrowserApplication browser,
|
||||
IModuleLogger logger,
|
||||
INativeMethods nativeMethods,
|
||||
ServiceProxy service,
|
||||
ProctoringSettings settings,
|
||||
IText text)
|
||||
{
|
||||
this.collector = new DataCollector(applicationMonitor, browser, logger.CloneFor(nameof(DataCollector)), nativeMethods, settings.ScreenProctoring);
|
||||
this.logger = logger;
|
||||
this.service = service;
|
||||
this.settings = settings.ScreenProctoring;
|
||||
this.spooler = new TransmissionSpooler(appConfig, logger.CloneFor(nameof(TransmissionSpooler)), service);
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
internal override void ExecuteRemainingWork()
|
||||
{
|
||||
logger.Info("Starting execution of remaining work...");
|
||||
spooler.ExecuteRemainingWork(InvokeRemainingWorkUpdated);
|
||||
logger.Info("Terminated execution of remaining work.");
|
||||
}
|
||||
|
||||
internal override bool HasRemainingWork()
|
||||
{
|
||||
var hasWork = spooler.HasRemainingWork();
|
||||
|
||||
if (hasWork)
|
||||
{
|
||||
logger.Info("There is remaining work to be done.");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Info("There is no remaining work to be done.");
|
||||
}
|
||||
|
||||
return hasWork;
|
||||
}
|
||||
|
||||
internal override void Initialize()
|
||||
{
|
||||
var start = true;
|
||||
|
||||
start &= !string.IsNullOrWhiteSpace(settings.ClientId);
|
||||
start &= !string.IsNullOrWhiteSpace(settings.ClientSecret);
|
||||
start &= !string.IsNullOrWhiteSpace(settings.GroupId);
|
||||
start &= !string.IsNullOrWhiteSpace(settings.ServiceUrl);
|
||||
|
||||
if (start)
|
||||
{
|
||||
logger.Info($"Initialized proctoring: All settings are valid, starting automatically...");
|
||||
|
||||
Connect();
|
||||
Start();
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateNotification(false);
|
||||
logger.Info($"Initialized proctoring: Not all settings are valid or a server session is active, not starting automatically.");
|
||||
}
|
||||
}
|
||||
|
||||
internal override void ProctoringConfigurationReceived(bool allowChat, bool receiveAudio, bool receiveVideo)
|
||||
{
|
||||
// Nothing to do here for now...
|
||||
}
|
||||
|
||||
internal override void ProctoringInstructionReceived(InstructionEventArgs args)
|
||||
{
|
||||
if (args is ScreenProctoringInstruction instruction)
|
||||
{
|
||||
logger.Info($"Proctoring instruction received: {instruction.Method}.");
|
||||
|
||||
if (instruction.Method == InstructionMethod.Join)
|
||||
{
|
||||
settings.ClientId = instruction.ClientId;
|
||||
settings.ClientSecret = instruction.ClientSecret;
|
||||
settings.GroupId = instruction.GroupId;
|
||||
settings.ServiceUrl = instruction.ServiceUrl;
|
||||
|
||||
Connect(instruction.SessionId);
|
||||
Start();
|
||||
}
|
||||
else
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
logger.Info("Successfully processed instruction.");
|
||||
}
|
||||
}
|
||||
|
||||
internal override void Start()
|
||||
{
|
||||
collector.DataCollected += Collector_DataCollected;
|
||||
collector.Start();
|
||||
spooler.Start();
|
||||
|
||||
UpdateNotification(true);
|
||||
|
||||
logger.Info($"Started proctoring.");
|
||||
}
|
||||
|
||||
internal override void Stop()
|
||||
{
|
||||
collector.Stop();
|
||||
collector.DataCollected -= Collector_DataCollected;
|
||||
spooler.Stop();
|
||||
|
||||
TerminateSession();
|
||||
UpdateNotification(false);
|
||||
|
||||
logger.Info("Stopped proctoring.");
|
||||
}
|
||||
|
||||
internal override void Terminate()
|
||||
{
|
||||
Stop();
|
||||
|
||||
logger.Info("Terminated proctoring.");
|
||||
}
|
||||
|
||||
private void Collector_DataCollected(MetaData metaData, ScreenShot screenShot)
|
||||
{
|
||||
spooler.Add(metaData, screenShot);
|
||||
}
|
||||
|
||||
private void Connect(string sessionId = default)
|
||||
{
|
||||
logger.Info("Connecting to service...");
|
||||
|
||||
var connect = service.Connect(settings.ClientId, settings.ClientSecret, settings.ServiceUrl);
|
||||
|
||||
if (connect.Success)
|
||||
{
|
||||
if (sessionId == default)
|
||||
{
|
||||
logger.Info("Creating session...");
|
||||
service.CreateSession(settings.GroupId);
|
||||
}
|
||||
else
|
||||
{
|
||||
service.SessionId = sessionId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void TerminateSession()
|
||||
{
|
||||
if (service.IsConnected)
|
||||
{
|
||||
logger.Info("Terminating session...");
|
||||
service.TerminateSession();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateNotification(bool active)
|
||||
{
|
||||
CanActivate = false;
|
||||
|
||||
if (active)
|
||||
{
|
||||
IconResource = new XamlIconResource { Uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/ScreenProctoring_Active.xaml") };
|
||||
Tooltip = text.Get(TextKey.Notification_ProctoringActiveTooltip);
|
||||
}
|
||||
else
|
||||
{
|
||||
IconResource = new XamlIconResource { Uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/ScreenProctoring_Inactive.xaml") };
|
||||
Tooltip = text.Get(TextKey.Notification_ProctoringInactiveTooltip);
|
||||
}
|
||||
|
||||
InvokeNotificationChanged();
|
||||
}
|
||||
}
|
||||
}
|
28
SafeExamBrowser.Proctoring/ScreenProctoring/Service/Api.cs
Normal file
28
SafeExamBrowser.Proctoring/ScreenProctoring/Service/Api.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Service
|
||||
{
|
||||
internal class Api
|
||||
{
|
||||
internal const string SESSION_ID = "%%_SESSION_ID_%%";
|
||||
|
||||
internal string AccessTokenEndpoint { get; set; }
|
||||
internal string HealthEndpoint { get; set; }
|
||||
internal string ScreenShotEndpoint { get; set; }
|
||||
internal string SessionEndpoint { get; set; }
|
||||
|
||||
internal Api()
|
||||
{
|
||||
AccessTokenEndpoint = "/oauth/token";
|
||||
HealthEndpoint = "/health";
|
||||
ScreenShotEndpoint = $"/seb-api/v1/session/{SESSION_ID}/screenshot";
|
||||
SessionEndpoint = "/seb-api/v1/session";
|
||||
}
|
||||
}
|
||||
}
|
119
SafeExamBrowser.Proctoring/ScreenProctoring/Service/Parser.cs
Normal file
119
SafeExamBrowser.Proctoring/ScreenProctoring/Service/Parser.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
using SafeExamBrowser.Proctoring.ScreenProctoring.Service.Requests;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Service
|
||||
{
|
||||
internal class Parser
|
||||
{
|
||||
private readonly ILogger logger;
|
||||
|
||||
internal Parser(ILogger logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
internal bool IsTokenExpired(HttpContent content)
|
||||
{
|
||||
var isExpired = false;
|
||||
|
||||
try
|
||||
{
|
||||
var json = JsonConvert.DeserializeObject(Extract(content)) as JObject;
|
||||
var error = json["error"].Value<string>();
|
||||
|
||||
isExpired = error?.Equals("invalid_token", StringComparison.OrdinalIgnoreCase) == true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error("Failed to parse token expiration content!", e);
|
||||
}
|
||||
|
||||
return isExpired;
|
||||
}
|
||||
|
||||
internal bool TryParseHealth(HttpResponseMessage response, out int health)
|
||||
{
|
||||
var success = false;
|
||||
|
||||
health = default;
|
||||
|
||||
try
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
internal bool TryParseOauth2Token(HttpContent content, out string oauth2Token)
|
||||
{
|
||||
oauth2Token = default;
|
||||
|
||||
try
|
||||
{
|
||||
var json = JsonConvert.DeserializeObject(Extract(content)) as JObject;
|
||||
|
||||
oauth2Token = json["access_token"].Value<string>();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error("Failed to parse Oauth2 token!", e);
|
||||
}
|
||||
|
||||
return oauth2Token != default;
|
||||
}
|
||||
|
||||
internal bool TryParseSessionId(HttpResponseMessage response, out string sessionId)
|
||||
{
|
||||
sessionId = default;
|
||||
|
||||
try
|
||||
{
|
||||
if (response.Headers.TryGetValues(Header.SESSION_ID, out var values))
|
||||
{
|
||||
sessionId = values.First();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error("Failed to parse session identifier!", e);
|
||||
}
|
||||
|
||||
return sessionId != default;
|
||||
}
|
||||
|
||||
private string Extract(HttpContent content)
|
||||
{
|
||||
var task = Task.Run(async () =>
|
||||
{
|
||||
return await content.ReadAsStreamAsync();
|
||||
});
|
||||
var stream = task.GetAwaiter().GetResult();
|
||||
var reader = new StreamReader(stream);
|
||||
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Service.Requests
|
||||
{
|
||||
internal static class ContentType
|
||||
{
|
||||
internal const string JSON = "application/json;charset=UTF-8";
|
||||
internal const string OCTET_STREAM = "application/octet-stream";
|
||||
internal const string URL_ENCODED = "application/x-www-form-urlencoded";
|
||||
}
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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.Net.Http;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Service.Requests
|
||||
{
|
||||
internal class CreateSessionRequest : Request
|
||||
{
|
||||
internal CreateSessionRequest(Api api, HttpClient httpClient, ILogger logger, Parser parser) : base(api, httpClient, logger, parser)
|
||||
{
|
||||
}
|
||||
|
||||
internal bool TryExecute(string groupId, out string message, out string sessionId)
|
||||
{
|
||||
var group = (Header.GROUP_ID, groupId);
|
||||
var success = TryExecute(HttpMethod.Post, api.SessionEndpoint, out var response, string.Empty, ContentType.URL_ENCODED, Authorization, group);
|
||||
|
||||
message = response.ToLogString();
|
||||
sessionId = default;
|
||||
|
||||
if (success)
|
||||
{
|
||||
parser.TryParseSessionId(response, out sessionId);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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.Net.Http;
|
||||
using System.Text;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Service.Requests
|
||||
{
|
||||
internal static class Extensions
|
||||
{
|
||||
internal static string ToLogString(this HttpResponseMessage response)
|
||||
{
|
||||
return response == default ? "No Response" : $"{(int) response.StatusCode} {response.StatusCode} {response.ReasonPhrase}";
|
||||
}
|
||||
|
||||
internal static string ToSummary(this Exception exception)
|
||||
{
|
||||
var trimChars = new[] { '.', '!' };
|
||||
var summary = new StringBuilder(exception.Message?.TrimEnd(trimChars));
|
||||
|
||||
for (var inner = exception.InnerException; inner != default; inner = inner.InnerException)
|
||||
{
|
||||
summary.Append($" -> {inner.Message?.TrimEnd(trimChars)}");
|
||||
}
|
||||
|
||||
return summary.ToString();
|
||||
}
|
||||
|
||||
internal static long ToUnixTimestamp(this DateTime date)
|
||||
{
|
||||
return new DateTimeOffset(date).ToUnixTimeMilliseconds();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Service.Requests
|
||||
{
|
||||
internal static class Header
|
||||
{
|
||||
internal const string ACCEPT = "Accept";
|
||||
internal const string AUTHORIZATION = "Authorization";
|
||||
internal const string GROUP_ID = "SEB_GROUP_UUID";
|
||||
internal const string HEALTH = "sps_server_health";
|
||||
internal const string IMAGE_FORMAT = "imageFormat";
|
||||
internal const string METADATA = "metaData";
|
||||
internal const string SESSION_ID = "SEB_SESSION_UUID";
|
||||
internal const string TIMESTAMP = "timestamp";
|
||||
|
||||
internal static class Metadata
|
||||
{
|
||||
internal const string ApplicationInfo = "screenProctoringMetadataApplication";
|
||||
internal const string BrowserInfo = "screenProctoringMetadataBrowser";
|
||||
internal const string BrowserUrls = "screenProctoringMetadataURL";
|
||||
internal const string TriggerInfo = "screenProctoringMetadataUserAction";
|
||||
internal const string WindowTitle = "screenProctoringMetadataWindowTitle";
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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.Net.Http;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Service.Requests
|
||||
{
|
||||
internal class HealthRequest : Request
|
||||
{
|
||||
internal HealthRequest(Api api, HttpClient httpClient, ILogger logger, Parser parser) : base(api, httpClient, logger, parser)
|
||||
{
|
||||
}
|
||||
|
||||
internal bool TryExecute(out int health, out string message)
|
||||
{
|
||||
var url = api.HealthEndpoint;
|
||||
var success = TryExecute(HttpMethod.Get, url, out var response);
|
||||
|
||||
health = default;
|
||||
message = response.ToLogString();
|
||||
|
||||
if (success)
|
||||
{
|
||||
parser.TryParseHealth(response, out health);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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.Net.Http;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Service.Requests
|
||||
{
|
||||
internal class OAuth2TokenRequest : Request
|
||||
{
|
||||
internal OAuth2TokenRequest(Api api, HttpClient httpClient, ILogger logger, Parser parser) : base(api, httpClient, logger, parser)
|
||||
{
|
||||
}
|
||||
|
||||
internal bool TryExecute(string clientId, string clientSecret, out string message)
|
||||
{
|
||||
ClientId = clientId;
|
||||
ClientSecret = clientSecret;
|
||||
|
||||
return TryRetrieveOAuth2Token(out message);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Service.Requests
|
||||
{
|
||||
internal abstract class Request
|
||||
{
|
||||
private const int ATTEMPTS = 5;
|
||||
|
||||
private static string oauth2Token;
|
||||
|
||||
private readonly HttpClient httpClient;
|
||||
|
||||
private bool hadException;
|
||||
|
||||
protected readonly Api api;
|
||||
protected readonly ILogger logger;
|
||||
protected readonly Parser parser;
|
||||
|
||||
protected static string ClientId { get; set; }
|
||||
protected static string ClientSecret { get; set; }
|
||||
|
||||
protected (string, string) Authorization => (Header.AUTHORIZATION, $"Bearer {oauth2Token}");
|
||||
|
||||
protected Request(Api api, HttpClient httpClient, ILogger logger, Parser parser)
|
||||
{
|
||||
this.api = api;
|
||||
this.httpClient = httpClient;
|
||||
this.logger = logger;
|
||||
this.parser = parser;
|
||||
}
|
||||
|
||||
protected bool TryExecute(
|
||||
HttpMethod method,
|
||||
string url,
|
||||
out HttpResponseMessage response,
|
||||
object content = default,
|
||||
string contentType = default,
|
||||
params (string name, string value)[] headers)
|
||||
{
|
||||
response = default;
|
||||
|
||||
for (var attempt = 0; attempt < ATTEMPTS && (response == default || !response.IsSuccessStatusCode); attempt++)
|
||||
{
|
||||
var request = BuildRequest(method, url, content, contentType, headers);
|
||||
|
||||
try
|
||||
{
|
||||
response = httpClient.SendAsync(request).GetAwaiter().GetResult();
|
||||
|
||||
logger.Debug($"Completed request: {request.Method} '{request.RequestUri}' -> {response.ToLogString()}");
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.Unauthorized && parser.IsTokenExpired(response.Content))
|
||||
{
|
||||
logger.Info("OAuth2 token has expired, attempting to retrieve new one...");
|
||||
|
||||
if (TryRetrieveOAuth2Token(out var message))
|
||||
{
|
||||
headers = UpdateOAuth2Token(headers);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
logger.Warn($"Request {request.Method} '{request.RequestUri}' did not complete within {httpClient.Timeout}!");
|
||||
|
||||
break;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (IsFirstException())
|
||||
{
|
||||
logger.Warn($"Request {request.Method} '{request.RequestUri}' has failed: {e.ToSummary()}!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response != default && response.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
protected bool TryRetrieveOAuth2Token(out string message)
|
||||
{
|
||||
var secret = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{ClientId}:{ClientSecret}"));
|
||||
var authorization = (Header.AUTHORIZATION, $"Basic {secret}");
|
||||
var content = "grant_type=client_credentials&scope=read write";
|
||||
var success = TryExecute(HttpMethod.Post, api.AccessTokenEndpoint, out var response, content, ContentType.URL_ENCODED, authorization);
|
||||
|
||||
message = response.ToLogString();
|
||||
|
||||
if (success && parser.TryParseOauth2Token(response.Content, out oauth2Token))
|
||||
{
|
||||
logger.Info("Successfully retrieved OAuth2 token.");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error("Failed to retrieve OAuth2 token!");
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
private HttpRequestMessage BuildRequest(
|
||||
HttpMethod method,
|
||||
string url,
|
||||
object content = default,
|
||||
string contentType = default,
|
||||
params (string name, string value)[] headers)
|
||||
{
|
||||
var request = new HttpRequestMessage(method, url);
|
||||
|
||||
if (content != default)
|
||||
{
|
||||
if (content is string)
|
||||
{
|
||||
request.Content = new StringContent(content as string, Encoding.UTF8);
|
||||
}
|
||||
|
||||
if (content is byte[])
|
||||
{
|
||||
request.Content = new ByteArrayContent(content as byte[]);
|
||||
}
|
||||
|
||||
if (contentType != default)
|
||||
{
|
||||
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType);
|
||||
}
|
||||
}
|
||||
|
||||
request.Headers.Add(Header.ACCEPT, "application/json, */*");
|
||||
|
||||
foreach (var (name, value) in headers)
|
||||
{
|
||||
request.Headers.Add(name, value);
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
private bool IsFirstException()
|
||||
{
|
||||
var isFirst = !hadException;
|
||||
|
||||
hadException = true;
|
||||
|
||||
return isFirst;
|
||||
}
|
||||
|
||||
private (string name, string value)[] UpdateOAuth2Token((string name, string value)[] headers)
|
||||
{
|
||||
var result = new List<(string name, string value)>();
|
||||
|
||||
foreach (var header in headers)
|
||||
{
|
||||
if (header.name == Header.AUTHORIZATION)
|
||||
{
|
||||
result.Add((Header.AUTHORIZATION, $"Bearer {oauth2Token}"));
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Add(header);
|
||||
}
|
||||
}
|
||||
|
||||
return result.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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.Net;
|
||||
using System.Net.Http;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
using SafeExamBrowser.Proctoring.ScreenProctoring.Data;
|
||||
using SafeExamBrowser.Proctoring.ScreenProctoring.Imaging;
|
||||
using SafeExamBrowser.Settings.Proctoring;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Service.Requests
|
||||
{
|
||||
internal class ScreenShotRequest : Request
|
||||
{
|
||||
internal ScreenShotRequest(Api api, HttpClient httpClient, ILogger logger, Parser parser) : base(api, httpClient, logger, parser)
|
||||
{
|
||||
}
|
||||
|
||||
internal bool TryExecute(MetaData metaData, ScreenShot screenShot, string sessionId, out int health, out string message)
|
||||
{
|
||||
var imageFormat = (Header.IMAGE_FORMAT, ToString(screenShot.Format));
|
||||
var metdataJson = (Header.METADATA, WebUtility.UrlEncode(metaData.ToJson()));
|
||||
var timestamp = (Header.TIMESTAMP, screenShot.CaptureTime.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);
|
||||
|
||||
health = default;
|
||||
message = response.ToLogString();
|
||||
|
||||
if (success)
|
||||
{
|
||||
parser.TryParseHealth(response, out health);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
private string ToString(ImageFormat format)
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case ImageFormat.Bmp:
|
||||
return "bmp";
|
||||
case ImageFormat.Gif:
|
||||
return "gif";
|
||||
case ImageFormat.Jpg:
|
||||
return "jpg";
|
||||
case ImageFormat.Png:
|
||||
return "png";
|
||||
default:
|
||||
throw new NotImplementedException($"Image format {format} is not yet implemented!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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.Net.Http;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Service.Requests
|
||||
{
|
||||
internal class TerminateSessionRequest : Request
|
||||
{
|
||||
internal TerminateSessionRequest(Api api, HttpClient httpClient, ILogger logger, Parser parser) : base(api, httpClient, logger, parser)
|
||||
{
|
||||
}
|
||||
|
||||
internal bool TryExecute(string sessionId, out string message)
|
||||
{
|
||||
var url = $"{api.SessionEndpoint}/{sessionId}";
|
||||
var success = TryExecute(HttpMethod.Delete, url, out var response, contentType: ContentType.URL_ENCODED, headers: Authorization);
|
||||
|
||||
message = response.ToLogString();
|
||||
|
||||
return success;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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.Net.Http;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
using SafeExamBrowser.Proctoring.ScreenProctoring.Data;
|
||||
using SafeExamBrowser.Proctoring.ScreenProctoring.Imaging;
|
||||
using SafeExamBrowser.Proctoring.ScreenProctoring.Service.Requests;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Service
|
||||
{
|
||||
internal class ServiceProxy
|
||||
{
|
||||
private readonly Api api;
|
||||
private readonly ILogger logger;
|
||||
private readonly Parser parser;
|
||||
|
||||
private HttpClient httpClient;
|
||||
|
||||
internal bool IsConnected => SessionId != default;
|
||||
internal string SessionId { get; set; }
|
||||
|
||||
internal ServiceProxy(ILogger logger)
|
||||
{
|
||||
this.api = new Api();
|
||||
this.logger = logger;
|
||||
this.parser = new Parser(logger);
|
||||
}
|
||||
|
||||
internal ServiceResponse Connect(string clientId, string clientSecret, string serviceUrl)
|
||||
{
|
||||
httpClient = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(serviceUrl),
|
||||
Timeout = TimeSpan.FromSeconds(10)
|
||||
};
|
||||
|
||||
var request = new OAuth2TokenRequest(api, httpClient, logger, parser);
|
||||
var success = request.TryExecute(clientId, clientSecret, out var message);
|
||||
|
||||
if (success)
|
||||
{
|
||||
logger.Info("Successfully connected to service.");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error("Failed to connect to service!");
|
||||
}
|
||||
|
||||
return new ServiceResponse(success, message);
|
||||
}
|
||||
|
||||
internal ServiceResponse CreateSession(string groupId)
|
||||
{
|
||||
var request = new CreateSessionRequest(api, httpClient, logger, parser);
|
||||
var success = request.TryExecute(groupId, out var message, out var sessionId);
|
||||
|
||||
if (success)
|
||||
{
|
||||
SessionId = sessionId;
|
||||
logger.Info("Successfully created session.");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error("Failed to create session!");
|
||||
}
|
||||
|
||||
return new ServiceResponse(success, message);
|
||||
}
|
||||
|
||||
internal ServiceResponse<int> GetHealth()
|
||||
{
|
||||
var request = new HealthRequest(api, httpClient, logger, parser);
|
||||
var success = request.TryExecute(out var health, out var message);
|
||||
|
||||
if (success)
|
||||
{
|
||||
logger.Info($"Successfully queried health (value: {health}).");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Warn("Failed to query health!");
|
||||
}
|
||||
|
||||
return new ServiceResponse<int>(success, health, message);
|
||||
}
|
||||
|
||||
internal ServiceResponse<int> Send(MetaData metaData, ScreenShot screenShot)
|
||||
{
|
||||
var request = new ScreenShotRequest(api, httpClient, logger, parser);
|
||||
var success = request.TryExecute(metaData, screenShot, SessionId, out var health, out var message);
|
||||
|
||||
if (success)
|
||||
{
|
||||
logger.Info($"Successfully sent screen shot ({screenShot}).");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error("Failed to send screen shot!");
|
||||
}
|
||||
|
||||
return new ServiceResponse<int>(success, health, message);
|
||||
}
|
||||
|
||||
internal ServiceResponse TerminateSession()
|
||||
{
|
||||
var request = new TerminateSessionRequest(api, httpClient, logger, parser);
|
||||
var success = request.TryExecute(SessionId, out var message);
|
||||
|
||||
if (success)
|
||||
{
|
||||
SessionId = default;
|
||||
logger.Info("Successfully terminated session.");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error("Failed to terminate session!");
|
||||
}
|
||||
|
||||
return new ServiceResponse(success, message);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring.Service
|
||||
{
|
||||
internal class ServiceResponse
|
||||
{
|
||||
internal string Message { get; }
|
||||
internal bool Success { get; }
|
||||
|
||||
internal ServiceResponse(bool success, string message = default)
|
||||
{
|
||||
Message = message;
|
||||
Success = success;
|
||||
}
|
||||
}
|
||||
|
||||
internal class ServiceResponse<T> : ServiceResponse
|
||||
{
|
||||
internal T Value { get; }
|
||||
|
||||
internal ServiceResponse(bool success, T value, string message = default) : base(success, message)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,421 @@
|
||||
/*
|
||||
* Copyright (c) 2024 ETH Zürich, IT Services
|
||||
*
|
||||
* 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.Concurrent;
|
||||
using System.Threading;
|
||||
using System.Timers;
|
||||
using SafeExamBrowser.Configuration.Contracts;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
using SafeExamBrowser.Proctoring.Contracts.Events;
|
||||
using SafeExamBrowser.Proctoring.ScreenProctoring.Data;
|
||||
using SafeExamBrowser.Proctoring.ScreenProctoring.Imaging;
|
||||
using SafeExamBrowser.Proctoring.ScreenProctoring.Service;
|
||||
using Timer = System.Timers.Timer;
|
||||
|
||||
namespace SafeExamBrowser.Proctoring.ScreenProctoring
|
||||
{
|
||||
internal class TransmissionSpooler
|
||||
{
|
||||
private const int BAD = 10;
|
||||
private const int GOOD = 0;
|
||||
|
||||
private readonly Buffer buffer;
|
||||
private readonly Cache cache;
|
||||
private readonly ILogger logger;
|
||||
private readonly ConcurrentQueue<(MetaData metaData, ScreenShot screenShot)> queue;
|
||||
private readonly Random random;
|
||||
private readonly ServiceProxy service;
|
||||
private readonly Timer timer;
|
||||
|
||||
private int health;
|
||||
private bool networkIssue;
|
||||
private bool recovering;
|
||||
private DateTime resume;
|
||||
private Thread thread;
|
||||
private CancellationTokenSource token;
|
||||
|
||||
internal TransmissionSpooler(AppConfig appConfig, IModuleLogger logger, ServiceProxy service)
|
||||
{
|
||||
this.buffer = new Buffer(logger.CloneFor(nameof(Buffer)));
|
||||
this.cache = new Cache(appConfig, logger.CloneFor(nameof(Cache)));
|
||||
this.logger = logger;
|
||||
this.queue = new ConcurrentQueue<(MetaData, ScreenShot)>();
|
||||
this.random = new Random();
|
||||
this.service = service;
|
||||
this.timer = new Timer();
|
||||
}
|
||||
|
||||
internal void Add(MetaData metaData, ScreenShot screenShot)
|
||||
{
|
||||
queue.Enqueue((metaData, screenShot));
|
||||
}
|
||||
|
||||
internal void ExecuteRemainingWork(Action<RemainingWorkUpdatedEventArgs> updateStatus)
|
||||
{
|
||||
var previous = buffer.Count + cache.Count;
|
||||
var progress = 0;
|
||||
var total = previous;
|
||||
|
||||
while (HasRemainingWork() && service.IsConnected && (!networkIssue || recovering || DateTime.Now < resume))
|
||||
{
|
||||
var remaining = buffer.Count + cache.Count;
|
||||
|
||||
if (total < remaining)
|
||||
{
|
||||
total = remaining;
|
||||
}
|
||||
else if (previous < remaining)
|
||||
{
|
||||
total += remaining - previous;
|
||||
}
|
||||
|
||||
previous = remaining;
|
||||
progress = total - remaining;
|
||||
|
||||
updateStatus(new RemainingWorkUpdatedEventArgs
|
||||
{
|
||||
IsWaiting = recovering || networkIssue,
|
||||
Next = buffer.TryPeek(out _, out var schedule, out _) ? schedule : default(DateTime?),
|
||||
Progress = progress,
|
||||
Resume = resume,
|
||||
Total = total
|
||||
});
|
||||
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
|
||||
if (networkIssue)
|
||||
{
|
||||
updateStatus(new RemainingWorkUpdatedEventArgs { HasFailed = true, CachePath = cache.Directory });
|
||||
}
|
||||
else
|
||||
{
|
||||
updateStatus(new RemainingWorkUpdatedEventArgs { IsFinished = true });
|
||||
}
|
||||
}
|
||||
|
||||
internal bool HasRemainingWork()
|
||||
{
|
||||
return buffer.Any() || cache.Any();
|
||||
}
|
||||
|
||||
internal void Start()
|
||||
{
|
||||
const int FIFTEEN_SECONDS = 15000;
|
||||
|
||||
logger.Debug("Starting...");
|
||||
|
||||
health = GOOD;
|
||||
recovering = false;
|
||||
resume = default;
|
||||
token = new CancellationTokenSource();
|
||||
|
||||
thread = new Thread(Execute);
|
||||
thread.IsBackground = true;
|
||||
thread.Start();
|
||||
|
||||
timer.AutoReset = false;
|
||||
timer.Elapsed += Timer_Elapsed;
|
||||
timer.Interval = FIFTEEN_SECONDS;
|
||||
}
|
||||
|
||||
internal void Stop()
|
||||
{
|
||||
const int TEN_SECONDS = 10000;
|
||||
|
||||
if (thread != default)
|
||||
{
|
||||
logger.Debug("Stopping...");
|
||||
|
||||
timer.Stop();
|
||||
timer.Elapsed -= Timer_Elapsed;
|
||||
|
||||
try
|
||||
{
|
||||
token.Cancel();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error("Failed to initiate execution cancellation!", e);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!thread.Join(TEN_SECONDS))
|
||||
{
|
||||
thread.Abort();
|
||||
logger.Warn($"Aborted execution since stopping gracefully within {TEN_SECONDS / 1000} seconds failed!");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error("Failed to stop!", e);
|
||||
}
|
||||
|
||||
recovering = false;
|
||||
resume = default;
|
||||
thread = default;
|
||||
token = default;
|
||||
}
|
||||
}
|
||||
|
||||
private void Execute()
|
||||
{
|
||||
logger.Debug("Ready.");
|
||||
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
if (health == BAD)
|
||||
{
|
||||
ExecuteCaching();
|
||||
}
|
||||
else if (recovering)
|
||||
{
|
||||
ExecuteRecovery();
|
||||
}
|
||||
else if (health == GOOD)
|
||||
{
|
||||
ExecuteNormally();
|
||||
}
|
||||
else
|
||||
{
|
||||
ExecuteDeferred();
|
||||
}
|
||||
|
||||
Thread.Sleep(50);
|
||||
}
|
||||
|
||||
logger.Debug("Stopped.");
|
||||
}
|
||||
|
||||
private void ExecuteCaching()
|
||||
{
|
||||
CacheFromBuffer();
|
||||
CacheFromQueue();
|
||||
}
|
||||
|
||||
private void ExecuteDeferred()
|
||||
{
|
||||
BufferFromCache();
|
||||
BufferFromQueue();
|
||||
TransmitFromBuffer();
|
||||
}
|
||||
|
||||
private void ExecuteNormally()
|
||||
{
|
||||
TransmitFromBuffer();
|
||||
TransmitFromCache();
|
||||
TransmitFromQueue();
|
||||
}
|
||||
|
||||
private void ExecuteRecovery()
|
||||
{
|
||||
recovering = DateTime.Now < resume;
|
||||
|
||||
if (recovering)
|
||||
{
|
||||
CacheFromQueue();
|
||||
}
|
||||
else
|
||||
{
|
||||
timer.Stop();
|
||||
logger.Info($"Recovery terminated, deactivating local caching and resuming transmission.");
|
||||
}
|
||||
}
|
||||
|
||||
private void BufferFromCache()
|
||||
{
|
||||
if (cache.TryDequeue(out var metaData, out var screenShot))
|
||||
{
|
||||
buffer.Enqueue(metaData, CalculateSchedule(metaData), screenShot);
|
||||
}
|
||||
}
|
||||
|
||||
private void BufferFromQueue()
|
||||
{
|
||||
if (TryDequeue(out var metaData, out var screenShot))
|
||||
{
|
||||
buffer.Enqueue(metaData, CalculateSchedule(metaData), screenShot);
|
||||
}
|
||||
}
|
||||
|
||||
private void CacheFromBuffer()
|
||||
{
|
||||
if (buffer.TryPeek(out var metaData, out _, out var screenShot) && cache.TryEnqueue(metaData, screenShot))
|
||||
{
|
||||
buffer.Dequeue();
|
||||
screenShot.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void CacheFromQueue()
|
||||
{
|
||||
if (TryDequeue(out var metaData, out var screenShot))
|
||||
{
|
||||
if (cache.TryEnqueue(metaData, screenShot))
|
||||
{
|
||||
screenShot.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer.Enqueue(metaData, CalculateSchedule(metaData), screenShot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private DateTime CalculateSchedule(MetaData metaData)
|
||||
{
|
||||
var timeout = (health + 1) * metaData.Elapsed.TotalMilliseconds;
|
||||
var schedule = DateTime.Now.AddMilliseconds(timeout);
|
||||
|
||||
return schedule;
|
||||
}
|
||||
|
||||
private void TransmitFromBuffer()
|
||||
{
|
||||
var hasItem = buffer.TryPeek(out var metaData, out var schedule, out var screenShot);
|
||||
var ready = schedule <= DateTime.Now;
|
||||
|
||||
if (hasItem && ready)
|
||||
{
|
||||
buffer.Dequeue();
|
||||
|
||||
if (TryTransmit(metaData, screenShot))
|
||||
{
|
||||
screenShot.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer.Enqueue(metaData, CalculateSchedule(metaData), screenShot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void TransmitFromCache()
|
||||
{
|
||||
if (cache.TryDequeue(out var metaData, out var screenShot))
|
||||
{
|
||||
if (TryTransmit(metaData, screenShot))
|
||||
{
|
||||
screenShot.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer.Enqueue(metaData, CalculateSchedule(metaData), screenShot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void TransmitFromQueue()
|
||||
{
|
||||
if (TryDequeue(out var metaData, out var screenShot))
|
||||
{
|
||||
if (TryTransmit(metaData, screenShot))
|
||||
{
|
||||
screenShot.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer.Enqueue(metaData, CalculateSchedule(metaData), screenShot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 TryTransmit(MetaData metaData, ScreenShot screenShot)
|
||||
{
|
||||
var success = false;
|
||||
|
||||
if (service.IsConnected)
|
||||
{
|
||||
var response = service.Send(metaData, screenShot);
|
||||
var value = response.Success ? response.Value : BAD;
|
||||
|
||||
health = UpdateHealth(value);
|
||||
networkIssue = !response.Success;
|
||||
success = response.Success;
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Warn("Cannot send screen shot as service is disconnected!");
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
private void Timer_Elapsed(object sender, ElapsedEventArgs e)
|
||||
{
|
||||
if (service.IsConnected)
|
||||
{
|
||||
var response = service.GetHealth();
|
||||
var value = response.Success ? response.Value : BAD;
|
||||
|
||||
health = UpdateHealth(value);
|
||||
networkIssue = !response.Success;
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Warn("Cannot query health as service is disconnected!");
|
||||
}
|
||||
|
||||
if (!timer.Enabled)
|
||||
{
|
||||
timer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
private int UpdateHealth(int value)
|
||||
{
|
||||
const int THREE_MINUTES = 180;
|
||||
|
||||
var previous = health;
|
||||
var current = value > BAD ? BAD : (value < GOOD ? GOOD : value);
|
||||
|
||||
if (previous != current)
|
||||
{
|
||||
logger.Info($"Service health {(previous < current ? "deteriorated" : "improved")} from {previous} to {current}.");
|
||||
|
||||
if (current == BAD)
|
||||
{
|
||||
recovering = false;
|
||||
resume = DateTime.Now.AddSeconds(random.Next(0, THREE_MINUTES));
|
||||
|
||||
if (!timer.Enabled)
|
||||
{
|
||||
timer.Start();
|
||||
}
|
||||
|
||||
logger.Warn($"Activating local caching and suspending transmission due to bad service health (resume: {resume:HH:mm:ss}).");
|
||||
}
|
||||
else if (previous == BAD)
|
||||
{
|
||||
recovering = true;
|
||||
resume = DateTime.Now < resume ? resume : DateTime.Now.AddSeconds(random.Next(0, THREE_MINUTES));
|
||||
|
||||
logger.Info($"Starting recovery while maintaining local caching (resume: {resume:HH:mm:ss}).");
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
}
|
||||
}
|
7
SafeExamBrowser.Proctoring/packages.config
Normal file
7
SafeExamBrowser.Proctoring/packages.config
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="KGySoft.CoreLibraries" version="8.1.0" targetFramework="net48" />
|
||||
<package id="KGySoft.Drawing" version="8.1.0" targetFramework="net48" />
|
||||
<package id="KGySoft.Drawing.Core" version="8.1.0" targetFramework="net48" />
|
||||
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net48" />
|
||||
</packages>
|
Reference in New Issue
Block a user