diff --git a/SafeExamBrowser.Client/ClientController.cs b/SafeExamBrowser.Client/ClientController.cs index 6448eed0..5c2aee2e 100644 --- a/SafeExamBrowser.Client/ClientController.cs +++ b/SafeExamBrowser.Client/ClientController.cs @@ -192,6 +192,7 @@ namespace SafeExamBrowser.Client ClientHost.PasswordRequested += ClientHost_PasswordRequested; ClientHost.ReconfigurationAborted += ClientHost_ReconfigurationAborted; ClientHost.ReconfigurationDenied += ClientHost_ReconfigurationDenied; + ClientHost.ServerFailureActionRequested += ClientHost_ServerFailureActionRequested; ClientHost.Shutdown += ClientHost_Shutdown; displayMonitor.DisplayChanged += DisplayMonitor_DisplaySettingsChanged; runtime.ConnectionLost += Runtime_ConnectionLost; @@ -233,6 +234,7 @@ namespace SafeExamBrowser.Client ClientHost.PasswordRequested -= ClientHost_PasswordRequested; ClientHost.ReconfigurationAborted -= ClientHost_ReconfigurationAborted; ClientHost.ReconfigurationDenied -= ClientHost_ReconfigurationDenied; + ClientHost.ServerFailureActionRequested -= ClientHost_ServerFailureActionRequested; ClientHost.Shutdown -= ClientHost_Shutdown; } @@ -463,6 +465,17 @@ namespace SafeExamBrowser.Client splashScreen.Hide(); } + private void ClientHost_ServerFailureActionRequested(ServerFailureActionRequestEventArgs args) + { + logger.Info($"Received server failure action request with id '{args.RequestId}'."); + + var dialog = uiFactory.CreateServerFailureDialog(args.Message, args.ShowFallback); + var result = dialog.Show(splashScreen); + + runtime.SubmitServerFailureActionResult(args.RequestId, result.Abort, result.Fallback, result.Retry); + logger.Info($"Server failure action request with id '{args.RequestId}' is complete."); + } + private void ClientHost_Shutdown() { shutdown.Invoke(); diff --git a/SafeExamBrowser.Client/Communication/ClientHost.cs b/SafeExamBrowser.Client/Communication/ClientHost.cs index c9051b26..484a2e69 100644 --- a/SafeExamBrowser.Client/Communication/ClientHost.cs +++ b/SafeExamBrowser.Client/Communication/ClientHost.cs @@ -30,6 +30,7 @@ namespace SafeExamBrowser.Client.Communication public event CommunicationEventHandler ReconfigurationAborted; public event CommunicationEventHandler ReconfigurationDenied; public event CommunicationEventHandler RuntimeDisconnected; + public event CommunicationEventHandler ServerFailureActionRequested; public event CommunicationEventHandler Shutdown; public ClientHost( @@ -82,6 +83,9 @@ namespace SafeExamBrowser.Client.Communication case ReconfigurationDeniedMessage m: ReconfigurationDenied?.InvokeAsync(new ReconfigurationEventArgs { ConfigurationPath = m.FilePath }); return new SimpleResponse(SimpleResponsePurport.Acknowledged); + case ServerFailureActionRequestMessage m: + ServerFailureActionRequested?.InvokeAsync(new ServerFailureActionRequestEventArgs { Message = m.Message, RequestId = m.RequestId, ShowFallback = m.ShowFallback }); + return new SimpleResponse(SimpleResponsePurport.Acknowledged); } return new SimpleResponse(SimpleResponsePurport.UnknownMessage); diff --git a/SafeExamBrowser.Communication.Contracts/Data/ServerFailureActionReplyMessage.cs b/SafeExamBrowser.Communication.Contracts/Data/ServerFailureActionReplyMessage.cs new file mode 100644 index 00000000..e460cffc --- /dev/null +++ b/SafeExamBrowser.Communication.Contracts/Data/ServerFailureActionReplyMessage.cs @@ -0,0 +1,47 @@ +/* + * 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/. + */ + +using System; + +namespace SafeExamBrowser.Communication.Contracts.Data +{ + /// + /// The reply to a . + /// + [Serializable] + public class ServerFailureActionReplyMessage : Message + { + /// + /// The user chose to abort the operation. + /// + public bool Abort { get; set; } + + /// + /// The user chose to perform a fallback. + /// + public bool Fallback { get; set; } + + /// + /// The user chose to retry the operation. + /// + public bool Retry { get; set; } + + /// + /// Identifies the server failure action request. + /// + public Guid RequestId { get; set; } + + public ServerFailureActionReplyMessage(bool abort, bool fallback, bool retry, Guid requestId) + { + Abort = abort; + Fallback = fallback; + Retry = retry; + RequestId = requestId; + } + } +} diff --git a/SafeExamBrowser.Communication.Contracts/Data/ServerFailureActionRequestMessage.cs b/SafeExamBrowser.Communication.Contracts/Data/ServerFailureActionRequestMessage.cs new file mode 100644 index 00000000..9c4a23db --- /dev/null +++ b/SafeExamBrowser.Communication.Contracts/Data/ServerFailureActionRequestMessage.cs @@ -0,0 +1,41 @@ +/* + * 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/. + */ + +using System; + +namespace SafeExamBrowser.Communication.Contracts.Data +{ + /// + /// This message is transmitted to the client to request a server failure action selection by the user. + /// + [Serializable] + public class ServerFailureActionRequestMessage : Message + { + /// + /// The server failure message, if available. + /// + public string Message { get; set; } + + /// + /// Indicates whether the fallback option should be shown to the user. + /// + public bool ShowFallback { get; set; } + + /// + /// The unique identifier for the server failure action selection request. + /// + public Guid RequestId { get; } + + public ServerFailureActionRequestMessage(string message, bool showFallback, Guid requestId) + { + Message = message; + ShowFallback = showFallback; + RequestId = requestId; + } + } +} diff --git a/SafeExamBrowser.Communication.Contracts/Events/ServerFailureActionReplyEventArgs.cs b/SafeExamBrowser.Communication.Contracts/Events/ServerFailureActionReplyEventArgs.cs new file mode 100644 index 00000000..f3ce4b46 --- /dev/null +++ b/SafeExamBrowser.Communication.Contracts/Events/ServerFailureActionReplyEventArgs.cs @@ -0,0 +1,38 @@ +/* + * 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/. + */ + +using System; + +namespace SafeExamBrowser.Communication.Contracts.Events +{ + /// + /// The event arguments used for the server failure action event fired by the . + /// + public class ServerFailureActionReplyEventArgs : CommunicationEventArgs + { + /// + /// The user chose to abort the operation. + /// + public bool Abort { get; set; } + + /// + /// The user chose to perform a fallback. + /// + public bool Fallback { get; set; } + + /// + /// The user chose to retry the operation. + /// + public bool Retry { get; set; } + + /// + /// Identifies the server failure action request. + /// + public Guid RequestId { get; set; } + } +} diff --git a/SafeExamBrowser.Communication.Contracts/Events/ServerFailureActionRequestEventArgs.cs b/SafeExamBrowser.Communication.Contracts/Events/ServerFailureActionRequestEventArgs.cs new file mode 100644 index 00000000..bfcbcb43 --- /dev/null +++ b/SafeExamBrowser.Communication.Contracts/Events/ServerFailureActionRequestEventArgs.cs @@ -0,0 +1,33 @@ +/* + * 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/. + */ + +using System; + +namespace SafeExamBrowser.Communication.Contracts.Events +{ + /// + /// The event arguments used for the server failure action request event fired by the . + /// + public class ServerFailureActionRequestEventArgs : CommunicationEventArgs + { + /// + /// The server failure message, if available. + /// + public string Message { get; set; } + + /// + /// Indicates whether the fallback option should be shown to the user. + /// + public bool ShowFallback { get; set; } + + /// + /// Identifies the server failure action selection request. + /// + public Guid RequestId { get; set; } + } +} diff --git a/SafeExamBrowser.Communication.Contracts/Hosts/IClientHost.cs b/SafeExamBrowser.Communication.Contracts/Hosts/IClientHost.cs index f6d58f90..3d90653e 100644 --- a/SafeExamBrowser.Communication.Contracts/Hosts/IClientHost.cs +++ b/SafeExamBrowser.Communication.Contracts/Hosts/IClientHost.cs @@ -56,6 +56,11 @@ namespace SafeExamBrowser.Communication.Contracts.Hosts /// event CommunicationEventHandler RuntimeDisconnected; + /// + /// Event fired when the runtime requests a server failure action selection from the user. + /// + event CommunicationEventHandler ServerFailureActionRequested; + /// /// Event fired when the runtime commands the client to shutdown. /// diff --git a/SafeExamBrowser.Communication.Contracts/Hosts/IRuntimeHost.cs b/SafeExamBrowser.Communication.Contracts/Hosts/IRuntimeHost.cs index 0325b85d..b049b7fe 100644 --- a/SafeExamBrowser.Communication.Contracts/Hosts/IRuntimeHost.cs +++ b/SafeExamBrowser.Communication.Contracts/Hosts/IRuntimeHost.cs @@ -61,6 +61,11 @@ namespace SafeExamBrowser.Communication.Contracts.Hosts /// event CommunicationEventHandler ReconfigurationRequested; + /// + /// Event fired when the client submitted a server failure action chosen by the user. + /// + event CommunicationEventHandler ServerFailureActionReceived; + /// /// Event fired when the client requests to shut down the application. /// diff --git a/SafeExamBrowser.Communication.Contracts/ICommunication.cs b/SafeExamBrowser.Communication.Contracts/ICommunication.cs index 6babcfce..c20c069d 100644 --- a/SafeExamBrowser.Communication.Contracts/ICommunication.cs +++ b/SafeExamBrowser.Communication.Contracts/ICommunication.cs @@ -29,6 +29,8 @@ namespace SafeExamBrowser.Communication.Contracts [ServiceKnownType(typeof(PasswordRequestMessage))] [ServiceKnownType(typeof(ReconfigurationMessage))] [ServiceKnownType(typeof(ReconfigurationDeniedMessage))] + [ServiceKnownType(typeof(ServerFailureActionReplyMessage))] + [ServiceKnownType(typeof(ServerFailureActionRequestMessage))] [ServiceKnownType(typeof(ServiceConfiguration))] [ServiceKnownType(typeof(SessionStartMessage))] [ServiceKnownType(typeof(SessionStopMessage))] diff --git a/SafeExamBrowser.Communication.Contracts/Proxies/IClientProxy.cs b/SafeExamBrowser.Communication.Contracts/Proxies/IClientProxy.cs index 5f1e26bf..e957855e 100644 --- a/SafeExamBrowser.Communication.Contracts/Proxies/IClientProxy.cs +++ b/SafeExamBrowser.Communication.Contracts/Proxies/IClientProxy.cs @@ -47,6 +47,11 @@ namespace SafeExamBrowser.Communication.Contracts.Proxies /// CommunicationResult RequestPassword(PasswordRequestPurpose purpose, Guid requestId); + /// + /// Requests the client to render a server failure action dialog and subsequently return the interaction result as separate message. + /// + CommunicationResult RequestServerFailureAction(string message, bool showFallback, Guid requestId); + /// /// Requests the client to render a message box and subsequently return the interaction result as separate message. /// diff --git a/SafeExamBrowser.Communication.Contracts/Proxies/IRuntimeProxy.cs b/SafeExamBrowser.Communication.Contracts/Proxies/IRuntimeProxy.cs index 17335730..489e7e2f 100644 --- a/SafeExamBrowser.Communication.Contracts/Proxies/IRuntimeProxy.cs +++ b/SafeExamBrowser.Communication.Contracts/Proxies/IRuntimeProxy.cs @@ -52,5 +52,10 @@ namespace SafeExamBrowser.Communication.Contracts.Proxies /// the password parameter will be ! /// CommunicationResult SubmitPassword(Guid requestId, bool success, string password = default(string)); + + /// + /// Submits the result of a server failure action selection previously requested by the runtime. + /// + CommunicationResult SubmitServerFailureActionResult(Guid requestId, bool abort, bool fallback, bool retry); } } diff --git a/SafeExamBrowser.Communication.Contracts/SafeExamBrowser.Communication.Contracts.csproj b/SafeExamBrowser.Communication.Contracts/SafeExamBrowser.Communication.Contracts.csproj index e50b4626..e12e8cfc 100644 --- a/SafeExamBrowser.Communication.Contracts/SafeExamBrowser.Communication.Contracts.csproj +++ b/SafeExamBrowser.Communication.Contracts/SafeExamBrowser.Communication.Contracts.csproj @@ -71,6 +71,8 @@ + + @@ -87,6 +89,8 @@ + + diff --git a/SafeExamBrowser.Communication/Proxies/ClientProxy.cs b/SafeExamBrowser.Communication/Proxies/ClientProxy.cs index e82d67af..e18e098c 100644 --- a/SafeExamBrowser.Communication/Proxies/ClientProxy.cs +++ b/SafeExamBrowser.Communication/Proxies/ClientProxy.cs @@ -180,6 +180,32 @@ namespace SafeExamBrowser.Communication.Proxies } } + public CommunicationResult RequestServerFailureAction(string message, bool showFallback, Guid requestId) + { + try + { + var response = Send(new ServerFailureActionRequestMessage(message, showFallback, requestId)); + var success = IsAcknowledged(response); + + if (success) + { + Logger.Debug("Client acknowledged server failure action request."); + } + else + { + Logger.Error($"Client did not acknowledge server failure action request! Received: {ToString(response)}."); + } + + return new CommunicationResult(success); + } + catch (Exception e) + { + Logger.Error($"Failed to perform '{nameof(RequestServerFailureAction)}'", e); + + return new CommunicationResult(false); + } + } + public CommunicationResult ShowMessage(string message, string title, int action, int icon, Guid requestId) { try diff --git a/SafeExamBrowser.Communication/Proxies/RuntimeProxy.cs b/SafeExamBrowser.Communication/Proxies/RuntimeProxy.cs index 5bf94ac6..bd153f0e 100644 --- a/SafeExamBrowser.Communication/Proxies/RuntimeProxy.cs +++ b/SafeExamBrowser.Communication/Proxies/RuntimeProxy.cs @@ -204,5 +204,31 @@ namespace SafeExamBrowser.Communication.Proxies return new CommunicationResult(false); } } + + public CommunicationResult SubmitServerFailureActionResult(Guid requestId, bool abort, bool fallback, bool retry) + { + try + { + var response = Send(new ServerFailureActionReplyMessage(abort, fallback, retry, requestId)); + var acknowledged = IsAcknowledged(response); + + if (acknowledged) + { + Logger.Debug("Runtime acknowledged server failure action transmission."); + } + else + { + Logger.Error($"Runtime did not acknowledge server failure action transmission! Response: {ToString(response)}."); + } + + return new CommunicationResult(acknowledged); + } + catch (Exception e) + { + Logger.Error($"Failed to perform '{nameof(SubmitServerFailureActionResult)}'", e); + + return new CommunicationResult(false); + } + } } } diff --git a/SafeExamBrowser.Runtime/Communication/RuntimeHost.cs b/SafeExamBrowser.Runtime/Communication/RuntimeHost.cs index af27eb22..789414d0 100644 --- a/SafeExamBrowser.Runtime/Communication/RuntimeHost.cs +++ b/SafeExamBrowser.Runtime/Communication/RuntimeHost.cs @@ -28,6 +28,7 @@ namespace SafeExamBrowser.Runtime.Communication public event CommunicationEventHandler MessageBoxReplyReceived; public event CommunicationEventHandler PasswordReceived; public event CommunicationEventHandler ReconfigurationRequested; + public event CommunicationEventHandler ServerFailureActionReceived; public event CommunicationEventHandler ShutdownRequested; public RuntimeHost(string address, IHostObjectFactory factory, ILogger logger, int timeout_ms) : base(address, factory, logger, timeout_ms) @@ -71,6 +72,9 @@ namespace SafeExamBrowser.Runtime.Communication case ReconfigurationMessage m: ReconfigurationRequested?.InvokeAsync(new ReconfigurationEventArgs { ConfigurationPath = m.ConfigurationPath }); return new SimpleResponse(SimpleResponsePurport.Acknowledged); + case ServerFailureActionReplyMessage m: + ServerFailureActionReceived?.InvokeAsync(new ServerFailureActionReplyEventArgs { Abort = m.Abort, Fallback = m.Fallback, RequestId = m.RequestId, Retry = m.Retry }); + return new SimpleResponse(SimpleResponsePurport.Acknowledged); } return new SimpleResponse(SimpleResponsePurport.UnknownMessage); diff --git a/SafeExamBrowser.Runtime/RuntimeController.cs b/SafeExamBrowser.Runtime/RuntimeController.cs index d4abdcc8..6e012783 100644 --- a/SafeExamBrowser.Runtime/RuntimeController.cs +++ b/SafeExamBrowser.Runtime/RuntimeController.cs @@ -567,7 +567,37 @@ namespace SafeExamBrowser.Runtime private void TryAskForServerFailureActionViaClient(ServerFailureEventArgs args) { - // TODO: Implement communication mechanism! + var requestId = Guid.NewGuid(); + var response = default(ServerFailureActionReplyEventArgs); + var responseEvent = new AutoResetEvent(false); + var responseEventHandler = new CommunicationEventHandler((a) => + { + if (a.RequestId == requestId) + { + response = a; + responseEvent.Set(); + } + }); + + runtimeHost.ServerFailureActionReceived += responseEventHandler; + + var communication = sessionContext.ClientProxy.RequestServerFailureAction(args.Message, args.ShowFallback, requestId); + + if (communication.Success) + { + responseEvent.WaitOne(); + args.Abort = response.Abort; + args.Fallback = response.Fallback; + args.Retry = response.Retry; + } + else + { + args.Abort = true; + args.Fallback = false; + args.Retry = false; + } + + runtimeHost.ServerFailureActionReceived -= responseEventHandler; } private void TryGetPasswordViaDialog(PasswordRequiredEventArgs args)