From 09fbc6579a4a1fb87bbfc3c3214d095aa3823a88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damian=20B=C3=BCchel?= Date: Sat, 1 Aug 2020 17:55:18 +0200 Subject: [PATCH] SEBWIN-405: Changed implementation for sending of log events and implemented server quit event. --- SafeExamBrowser.Client/ClientController.cs | 10 +- .../Operations/ServerOperation.cs | 1 + .../TerminationRequestedEventHandler.cs | 15 +++ .../IServerProxy.cs | 6 ++ .../SafeExamBrowser.Server.Contracts.csproj | 2 + SafeExamBrowser.Server/ServerProxy.cs | 98 ++++++++++++++----- 6 files changed, 104 insertions(+), 28 deletions(-) create mode 100644 SafeExamBrowser.Server.Contracts/Events/TerminationRequestedEventHandler.cs diff --git a/SafeExamBrowser.Client/ClientController.cs b/SafeExamBrowser.Client/ClientController.cs index 5c2aee2e..11f5c5bc 100644 --- a/SafeExamBrowser.Client/ClientController.cs +++ b/SafeExamBrowser.Client/ClientController.cs @@ -206,7 +206,7 @@ namespace SafeExamBrowser.Client if (Server != null) { - // TODO + Server.TerminationRequested += Server_TerminationRequested; } } @@ -240,7 +240,7 @@ namespace SafeExamBrowser.Client if (Server != null) { - // TODO + Server.TerminationRequested -= Server_TerminationRequested; } foreach (var activator in context.Activators.OfType()) @@ -551,6 +551,12 @@ namespace SafeExamBrowser.Client shutdown.Invoke(); } + private void Server_TerminationRequested() + { + logger.Info("Attempting to shutdown as requested by the server..."); + TryRequestShutdown(); + } + private void Shell_QuitButtonClicked(System.ComponentModel.CancelEventArgs args) { PauseActivators(); diff --git a/SafeExamBrowser.Runtime/Operations/ServerOperation.cs b/SafeExamBrowser.Runtime/Operations/ServerOperation.cs index dcf56c69..2b03d6e8 100644 --- a/SafeExamBrowser.Runtime/Operations/ServerOperation.cs +++ b/SafeExamBrowser.Runtime/Operations/ServerOperation.cs @@ -77,6 +77,7 @@ namespace SafeExamBrowser.Runtime.Operations if (status == LoadStatus.Success) { + // TODO: Why aren't the server settings and SEB mode correctly set in the exam configuration? var serverSettings = Context.Next.Settings.Server; Context.Next.AppConfig.ServerApi = info.Api; diff --git a/SafeExamBrowser.Server.Contracts/Events/TerminationRequestedEventHandler.cs b/SafeExamBrowser.Server.Contracts/Events/TerminationRequestedEventHandler.cs new file mode 100644 index 00000000..dd775f59 --- /dev/null +++ b/SafeExamBrowser.Server.Contracts/Events/TerminationRequestedEventHandler.cs @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +namespace SafeExamBrowser.Server.Contracts.Events +{ + /// + /// Event handler used to indicate that a termination instruction has been detected. + /// + public delegate void TerminationRequestedEventHandler(); +} diff --git a/SafeExamBrowser.Server.Contracts/IServerProxy.cs b/SafeExamBrowser.Server.Contracts/IServerProxy.cs index fc2986f3..2bf43432 100644 --- a/SafeExamBrowser.Server.Contracts/IServerProxy.cs +++ b/SafeExamBrowser.Server.Contracts/IServerProxy.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using SafeExamBrowser.Server.Contracts.Data; +using SafeExamBrowser.Server.Contracts.Events; using SafeExamBrowser.Settings.Server; namespace SafeExamBrowser.Server.Contracts @@ -18,6 +19,11 @@ namespace SafeExamBrowser.Server.Contracts /// public interface IServerProxy { + /// + /// Event fired when the server detects an instruction to terminate SEB. + /// + event TerminationRequestedEventHandler TerminationRequested; + /// /// Attempts to initialize a connection with the server. /// diff --git a/SafeExamBrowser.Server.Contracts/SafeExamBrowser.Server.Contracts.csproj b/SafeExamBrowser.Server.Contracts/SafeExamBrowser.Server.Contracts.csproj index b5e6086e..4bc8ec6b 100644 --- a/SafeExamBrowser.Server.Contracts/SafeExamBrowser.Server.Contracts.csproj +++ b/SafeExamBrowser.Server.Contracts/SafeExamBrowser.Server.Contracts.csproj @@ -56,6 +56,7 @@ + @@ -66,5 +67,6 @@ SafeExamBrowser.Settings + \ No newline at end of file diff --git a/SafeExamBrowser.Server/ServerProxy.cs b/SafeExamBrowser.Server/ServerProxy.cs index ff318746..48a01ca0 100644 --- a/SafeExamBrowser.Server/ServerProxy.cs +++ b/SafeExamBrowser.Server/ServerProxy.cs @@ -14,6 +14,7 @@ using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Text; +using System.Threading; using System.Threading.Tasks; using System.Timers; using Newtonsoft.Json; @@ -22,9 +23,11 @@ using SafeExamBrowser.Configuration.Contracts; using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.Server.Contracts; using SafeExamBrowser.Server.Contracts.Data; +using SafeExamBrowser.Server.Contracts.Events; using SafeExamBrowser.Server.Data; using SafeExamBrowser.Settings.Logging; using SafeExamBrowser.Settings.Server; +using Timer = System.Timers.Timer; namespace SafeExamBrowser.Server { @@ -32,6 +35,7 @@ namespace SafeExamBrowser.Server { private ApiVersion1 api; private AppConfig appConfig; + private CancellationTokenSource cancellationTokenSource; private string connectionToken; private string examId; private HttpClient httpClient; @@ -40,12 +44,16 @@ namespace SafeExamBrowser.Server private string oauth2Token; private int pingNumber; private ServerSettings settings; + private Task task; private Timer timer; + public event TerminationRequestedEventHandler TerminationRequested; + public ServerProxy(AppConfig appConfig, ILogger logger) { this.api = new ApiVersion1(); this.appConfig = appConfig; + this.cancellationTokenSource = new CancellationTokenSource(); this.httpClient = new HttpClient(); this.logger = logger; this.logContent = new ConcurrentQueue(); @@ -244,6 +252,9 @@ namespace SafeExamBrowser.Server logger.Subscribe(this); + task = new Task(SendLog, cancellationTokenSource.Token); + task.Start(); + timer.AutoReset = false; timer.Elapsed += Timer_Elapsed; timer.Interval = 1000; @@ -253,25 +264,60 @@ namespace SafeExamBrowser.Server public void StopConnectivity() { logger.Unsubscribe(this); + cancellationTokenSource.Cancel(); + task.Wait(); timer.Stop(); timer.Elapsed -= Timer_Elapsed; } - private void Timer_Elapsed(object sender, ElapsedEventArgs args) + private void SendLog() { var authorization = ("Authorization", $"Bearer {oauth2Token}"); + var contentType = "application/json;charset=UTF-8"; var token = ("SEBConnectionToken", connectionToken); + logger.Info("Starting to send log items..."); + + while (!cancellationTokenSource.IsCancellationRequested) + { + try + { + if (logContent.TryDequeue(out var c) && c is ILogMessage message) + { + var json = new JObject + { + ["type"] = ToLogType(message.Severity), + ["timestamp"] = message.DateTime.Ticks, + ["text"] = message.Message + }; + var content = json.ToString(); + // TODO: Why can't we send multiple log messages in one request? + var success = TryExecute(HttpMethod.Post, api.LogEndpoint, out var response, content, contentType, authorization, token); + } + } + catch (Exception e) + { + logger.Error("Failed to send log!", e); + } + } + + logger.Info("Stopped sending log items."); + } + + private void Timer_Elapsed(object sender, ElapsedEventArgs args) + { try { + var authorization = ("Authorization", $"Bearer {oauth2Token}"); var content = $"timestamp={DateTime.Now.Ticks}&ping-number={++pingNumber}"; var contentType = "application/x-www-form-urlencoded"; + var token = ("SEBConnectionToken", connectionToken); var success = TryExecute(HttpMethod.Post, api.PingEndpoint, out var response, content, contentType, authorization, token); - if (success) + if (success && TryParseInstruction(response.Content, out var instruction) && instruction == "SEB_QUIT") { - // TODO: Fire event if instruction is sent via response! + Task.Run(() => TerminationRequested?.Invoke()); } else { @@ -283,28 +329,6 @@ namespace SafeExamBrowser.Server logger.Error("Failed to send ping!", e); } - try - { - if (logContent.TryDequeue(out var c) && c is ILogMessage message) - { - var json = new JObject - { - ["type"] = ToLogType(message.Severity), - ["timestamp"] = message.DateTime.Ticks, - ["text"] = message.Message - }; - - var content = json.ToString(); - var contentType = "application/json;charset=UTF-8"; - // TODO: Why can't we send multiple log messages in one request? - var success = TryExecute(HttpMethod.Post, api.LogEndpoint, out var response, content, contentType, authorization, token); - } - } - catch (Exception e) - { - logger.Error("Failed to send log!", e); - } - timer.Start(); } @@ -413,6 +437,24 @@ namespace SafeExamBrowser.Server return exams.Any(); } + private bool TryParseInstruction(HttpContent content, out string instruction) + { + instruction = default(string); + + try + { + var json = JsonConvert.DeserializeObject(Extract(content)) as JObject; + + instruction = json["instruction"].Value(); + } + catch (Exception e) + { + logger.Error("Failed to parse instruction!", e); + } + + return instruction != default(string); + } + private bool TryParseOauth2Token(HttpContent content) { try @@ -461,7 +503,11 @@ namespace SafeExamBrowser.Server try { response = httpClient.SendAsync(request).GetAwaiter().GetResult(); - logger.Debug($"Completed request: {request.Method} '{request.RequestUri}' -> {ToString(response)}"); + + if (request.RequestUri.AbsolutePath != api.LogEndpoint && request.RequestUri.AbsolutePath != api.PingEndpoint) + { + logger.Debug($"Completed request: {request.Method} '{request.RequestUri}' -> {ToString(response)}"); + } } catch (TaskCanceledException) {