SEBWIN-219: Extended IPC mechanics with ping mechanism.

This commit is contained in:
dbuechel 2018-02-27 15:28:54 +01:00
parent 268eda9f90
commit 8a06a0fe98
15 changed files with 263 additions and 63 deletions

View file

@ -12,6 +12,7 @@ using SafeExamBrowser.Contracts.Behaviour.Operations;
using SafeExamBrowser.Contracts.Communication;
using SafeExamBrowser.Contracts.Configuration;
using SafeExamBrowser.Contracts.Configuration.Settings;
using SafeExamBrowser.Contracts.I18n;
using SafeExamBrowser.Contracts.Logging;
using SafeExamBrowser.Contracts.Monitoring;
using SafeExamBrowser.Contracts.UserInterface;
@ -81,13 +82,21 @@ namespace SafeExamBrowser.Client.Behaviour
var success = operations.TryPerform();
// TODO
if (success)
{
RegisterEvents();
// TODO: Handle communication exception!
runtime.InformClientReady();
try
{
runtime.InformClientReady();
}
catch (Exception e)
{
logger.Error("Failed to inform runtime that client is ready!", e);
return false;
}
splashScreen.Hide();
logger.Info("--- Application successfully initialized ---");
@ -110,9 +119,8 @@ namespace SafeExamBrowser.Client.Behaviour
splashScreen.Show();
splashScreen.BringToForeground();
// TODO
DeregisterEvents();
var success = operations.TryRevert();
if (success)
@ -134,6 +142,7 @@ namespace SafeExamBrowser.Client.Behaviour
ClientHost.Shutdown += ClientHost_Shutdown;
displayMonitor.DisplayChanged += DisplayMonitor_DisplaySettingsChanged;
processMonitor.ExplorerStarted += ProcessMonitor_ExplorerStarted;
runtime.ConnectionLost += Runtime_ConnectionLost;
taskbar.QuitButtonClicked += Taskbar_QuitButtonClicked;
windowMonitor.WindowChanged += WindowMonitor_WindowChanged;
}
@ -143,6 +152,7 @@ namespace SafeExamBrowser.Client.Behaviour
ClientHost.Shutdown -= ClientHost_Shutdown;
displayMonitor.DisplayChanged -= DisplayMonitor_DisplaySettingsChanged;
processMonitor.ExplorerStarted -= ProcessMonitor_ExplorerStarted;
runtime.ConnectionLost -= Runtime_ConnectionLost;
taskbar.QuitButtonClicked -= Taskbar_QuitButtonClicked;
windowMonitor.WindowChanged -= WindowMonitor_WindowChanged;
}
@ -173,11 +183,31 @@ namespace SafeExamBrowser.Client.Behaviour
shutdown.Invoke();
}
private void Runtime_ConnectionLost()
{
logger.Error("Lost connection to the runtime!");
uiFactory.Show(TextKey.MessageBox_ApplicationError, TextKey.MessageBox_ApplicationErrorTitle, icon: MessageBoxIcon.Error);
taskbar.Close();
shutdown.Invoke();
}
private void Taskbar_QuitButtonClicked()
{
// TODO: MessageBox asking whether user really wants to quit -> only then request shutdown!
// TODO: Handle communication exception!
runtime.RequestShutdown();
var result = uiFactory.Show(TextKey.MessageBox_Quit, TextKey.MessageBox_QuitTitle, MessageBoxAction.YesNo, MessageBoxIcon.Question);
if (result == MessageBoxResult.Yes)
{
try
{
runtime.RequestShutdown();
}
catch (Exception e)
{
logger.Error("Failed to communicate shutdown request to the runtime!", e);
uiFactory.Show(TextKey.MessageBox_QuitError, TextKey.MessageBox_QuitErrorTitle, icon: MessageBoxIcon.Error);
}
}
}
private void WindowMonitor_WindowChanged(IntPtr window)

View file

@ -17,6 +17,7 @@ namespace SafeExamBrowser.Client.Communication
{
internal class ClientHost : BaseHost, IClientHost
{
private bool allowConnection = true;
private int processId;
public Guid StartupToken { private get; set; }
@ -30,18 +31,24 @@ namespace SafeExamBrowser.Client.Communication
protected override bool OnConnect(Guid? token)
{
return StartupToken == token;
var authenticated = StartupToken == token;
var accepted = allowConnection && authenticated;
if (accepted)
{
allowConnection = false;
}
return accepted;
}
protected override void OnDisconnect()
{
// TODO
// Nothing to do here...
}
protected override Response OnReceive(Message message)
{
// TODO
return new SimpleResponse(SimpleResponsePurport.UnknownMessage);
}

View file

@ -13,7 +13,13 @@ namespace SafeExamBrowser.Contracts.Communication
public interface ICommunicationProxy
{
/// <summary>
/// Tries to establish a connection. Returns <c>true</c> if the connection has been accepted, otherwise <c>false</c>.
/// Fired when the connection to the proxy was lost, e.g. if a ping request failed or a communication fault occurred.
/// </summary>
event CommunicationEventHandler ConnectionLost;
/// <summary>
/// Tries to establish a connection. Returns <c>true</c> if the connection has been accepted, otherwise <c>false</c>. If a
/// connection was successfully established, a ping mechanism will be activated to periodically check the connection status.
/// </summary>
/// <exception cref="System.ServiceModel.*">If the communication failed.</exception>
bool Connect(Guid? token = null);

View file

@ -11,7 +11,7 @@ using System;
namespace SafeExamBrowser.Contracts.Communication.Messages
{
[Serializable]
public class Message
public abstract class Message
{
/// <summary>
/// The communication token needed for authentication.

View file

@ -28,6 +28,11 @@ namespace SafeExamBrowser.Contracts.Communication.Messages
/// </summary>
ConfigurationNeeded,
/// <summary>
/// Requests an interlocutor to signal that the connection status is okay.
/// </summary>
Ping,
/// <summary>
/// Sent from the client to the runtime to request shutting down the application.
/// </summary>

View file

@ -11,7 +11,7 @@ using System;
namespace SafeExamBrowser.Contracts.Communication.Responses
{
[Serializable]
public class Response
public abstract class Response
{
public override string ToString()
{

View file

@ -15,10 +15,18 @@ namespace SafeExamBrowser.Contracts.I18n
{
Browser_ShowDeveloperConsole,
LogWindow_Title,
MessageBox_ApplicationError,
MessageBox_ApplicationErrorTitle,
MessageBox_ConfigureClientSuccess,
MessageBox_ConfigureClientSuccessTitle,
MessageBox_Quit,
MessageBox_QuitTitle,
MessageBox_QuitError,
MessageBox_QuitErrorTitle,
MessageBox_SessionStartError,
MessageBox_SessionStartErrorTitle,
MessageBox_SessionStopError,
MessageBox_SessionStopErrorTitle,
MessageBox_ShutdownError,
MessageBox_ShutdownErrorTitle,
MessageBox_SingleInstance,

View file

@ -101,13 +101,17 @@ namespace SafeExamBrowser.Core.Communication
if (IsAuthorized(message?.CommunicationToken))
{
if (message is SimpleMessage simpleMessage)
switch (message)
{
response = OnReceive(simpleMessage.Purport);
}
else
{
response = OnReceive(message);
case SimpleMessage simpleMessage when simpleMessage.Purport == SimpleMessagePurport.Ping:
response = new SimpleResponse(SimpleResponsePurport.Acknowledged);
break;
case SimpleMessage simpleMessage:
response = OnReceive(simpleMessage.Purport);
break;
default:
response = OnReceive(message);
break;
}
}
@ -218,7 +222,7 @@ namespace SafeExamBrowser.Core.Communication
private void Host_Faulted(object sender, EventArgs e)
{
logger.Debug("Communication host has faulted!");
logger.Error("Communication host has faulted!");
}
private void Host_Opened(object sender, EventArgs e)
@ -233,7 +237,7 @@ namespace SafeExamBrowser.Core.Communication
private void Host_UnknownMessageReceived(object sender, UnknownMessageReceivedEventArgs e)
{
logger.Debug($"Communication host has received an unknown message: {e?.Message}.");
logger.Warn($"Communication host has received an unknown message: {e?.Message}.");
}
private string ToString(Message message)

View file

@ -8,6 +8,7 @@
using System;
using System.ServiceModel;
using System.Timers;
using SafeExamBrowser.Contracts.Communication;
using SafeExamBrowser.Contracts.Communication.Messages;
using SafeExamBrowser.Contracts.Communication.Responses;
@ -17,12 +18,18 @@ namespace SafeExamBrowser.Core.Communication
{
public abstract class BaseProxy : ICommunicationProxy
{
private const int ONE_MINUTE = 60000;
private static readonly object @lock = new object();
private string address;
private ICommunication channel;
private Guid? communicationToken;
private Timer timer;
protected ILogger Logger { get; private set; }
public event CommunicationEventHandler ConnectionLost;
public BaseProxy(string address, ILogger logger)
{
this.address = address;
@ -47,12 +54,18 @@ namespace SafeExamBrowser.Core.Communication
communicationToken = response.CommunicationToken;
Logger.Debug($"Connection was {(response.ConnectionEstablished ? "established" : "refused")}.");
if (response.ConnectionEstablished)
{
StartAutoPing();
}
return response.ConnectionEstablished;
}
public virtual bool Disconnect()
{
FailIfNotConnected(nameof(Disconnect));
StopAutoPing();
var message = new DisconnectionMessage { CommunicationToken = communicationToken.Value };
var response = channel.Disconnect(message);
@ -80,6 +93,11 @@ namespace SafeExamBrowser.Core.Communication
return Send(new SimpleMessage(purport));
}
protected bool IsAcknowledged(Response response)
{
return response is SimpleResponse simpleResponse && simpleResponse.Purport == SimpleResponsePurport.Acknowledged;
}
protected string ToString(Message message)
{
return message != null ? message.ToString() : "<null>";
@ -102,7 +120,8 @@ namespace SafeExamBrowser.Core.Communication
private void BaseProxy_Faulted(object sender, EventArgs e)
{
Logger.Debug("Communication channel has faulted!");
Logger.Error("Communication channel has faulted!");
ConnectionLost?.Invoke();
}
private void BaseProxy_Opened(object sender, EventArgs e)
@ -132,5 +151,54 @@ namespace SafeExamBrowser.Core.Communication
{
return channel == null ? "null" : $"in state '{(channel as ICommunicationObject).State}'";
}
private void StartAutoPing()
{
lock (@lock)
{
timer = new Timer(ONE_MINUTE) { AutoReset = true };
timer.Elapsed += Timer_Elapsed;
timer.Start();
}
}
private void StopAutoPing()
{
lock (@lock)
{
timer?.Stop();
}
}
private void Timer_Elapsed(object sender, ElapsedEventArgs args)
{
lock (@lock)
{
if (timer.Enabled)
{
try
{
var response = Send(SimpleMessagePurport.Ping);
if (IsAcknowledged(response))
{
Logger.Info("Pinged proxy, connection is alive.");
}
else
{
Logger.Error($"Proxy did not acknowledge ping message! Received: {ToString(response)}.");
timer.Stop();
ConnectionLost?.Invoke();
}
}
catch (Exception e)
{
Logger.Error("Failed to ping proxy!", e);
timer.Stop();
ConnectionLost?.Invoke();
}
}
}
}
}
}

View file

@ -6,6 +6,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System.ServiceModel;
using SafeExamBrowser.Contracts.Communication;
using SafeExamBrowser.Contracts.Communication.Messages;
using SafeExamBrowser.Contracts.Communication.Responses;
@ -23,9 +24,9 @@ namespace SafeExamBrowser.Core.Communication
{
var response = Send(SimpleMessagePurport.Shutdown);
if (response is SimpleResponse simpleMessage && simpleMessage.Purport == SimpleResponsePurport.Acknowledged)
if (!IsAcknowledged(response))
{
// TODO
throw new CommunicationException($"Runtime did not acknowledge shutdown request! Received: {ToString(response)}.");
}
}
@ -33,7 +34,12 @@ namespace SafeExamBrowser.Core.Communication
{
var response = Send(SimpleMessagePurport.Authenticate);
return response as AuthenticationResponse;
if (response is AuthenticationResponse authenticationResponse)
{
return authenticationResponse;
}
throw new CommunicationException($"Did not receive authentication response! Received: {ToString(response)}.");
}
}
}

View file

@ -52,10 +52,5 @@ namespace SafeExamBrowser.Core.Communication
throw new CommunicationException($"Runtime did not acknowledge shutdown request! Response: {ToString(response)}.");
}
}
private bool IsAcknowledged(Response response)
{
return response is SimpleResponse simpleResponse && simpleResponse.Purport == SimpleResponsePurport.Acknowledged;
}
}
}

View file

@ -6,18 +6,42 @@
<Entry key="LogWindow_Title">
Application Log
</Entry>
<Entry key="MessageBox_ApplicationError">
An unrecoverable error has occurred! Please consult the application log for more information. The application will now shut down...
</Entry>
<Entry key="MessageBox_ApplicationErrorTitle">
Application Error
</Entry>
<Entry key="MessageBox_ConfigureClientSuccess">
The client configuration has been saved and will be used when you start the application the next time. Do you want to quit for now?
</Entry>
<Entry key="MessageBox_ConfigureClientSuccessTitle">
Configuration Successful
</Entry>
<Entry key="MessageBox_Quit">
Would you really like to quit the application?
</Entry>
<Entry key="MessageBox_QuitTitle">
Quit?
</Entry>
<Entry key="MessageBox_QuitError">
The client failed to communicate the shutdown request to the runtime! Please try again...
</Entry>
<Entry key="MessageBox_QuitErrorTitle">
Quit Error
</Entry>
<Entry key="MessageBox_SessionStartError">
The application failed to start a new session. Please consult the application log for more information...
</Entry>
<Entry key="MessageBox_SessionStartErrorTitle">
Session Start Error
</Entry>
<Entry key="MessageBox_SessionStopError">
The application failed to properly stop the running session. Please consult the application log for more information...
</Entry>
<Entry key="MessageBox_SessionStopErrorTitle">
Session Stop Error
</Entry>
<Entry key="MessageBox_ShutdownError">
An unexpected error occurred during the shutdown procedure! Please consult the application log for more information...
</Entry>

View file

@ -6,7 +6,6 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Threading;
using SafeExamBrowser.Contracts.Behaviour.Operations;
using SafeExamBrowser.Contracts.Communication;
@ -65,14 +64,7 @@ namespace SafeExamBrowser.Runtime.Behaviour.Operations
logger.Info("Initializing service session...");
service.StartSession(session.Id, configuration.CurrentSettings);
try
{
sessionRunning = TryStartClient();
}
catch (Exception e)
{
logger.Error("Failed to start client!", e);
}
sessionRunning = TryStartClient();
if (sessionRunning)
{
@ -96,14 +88,10 @@ namespace SafeExamBrowser.Runtime.Behaviour.Operations
logger.Info("Stopping service session...");
service.StopSession(session.Id);
try
if (!session.ClientProcess.HasTerminated)
{
StopClient();
}
catch (Exception e)
{
logger.Error("Failed to terminate client!", e);
}
sessionRunning = false;
logger.Info($"Successfully stopped session with identifier '{session.Id}'.");
@ -204,7 +192,7 @@ namespace SafeExamBrowser.Runtime.Behaviour.Operations
}
else
{
logger.Warn("Attempting to kill client process since graceful shutdown failed!");
logger.Warn("Attempting to kill client process since graceful termination failed!");
KillClient();
}
}

View file

@ -75,12 +75,12 @@ namespace SafeExamBrowser.Runtime.Behaviour
if (initialized)
{
RegisterEvents();
logger.Info("--- Application successfully initialized ---");
logger.Log(string.Empty);
logger.Subscribe(runtimeWindow);
splashScreen.Hide();
runtimeHost.ShutdownRequested += RuntimeHost_ShutdownRequested;
StartSession(true);
}
@ -88,6 +88,8 @@ namespace SafeExamBrowser.Runtime.Behaviour
{
logger.Info("--- Application startup aborted! ---");
logger.Log(string.Empty);
uiFactory.Show(TextKey.MessageBox_StartupError, TextKey.MessageBox_StartupErrorTitle, icon: MessageBoxIcon.Error);
}
return initialized && sessionRunning;
@ -95,8 +97,11 @@ namespace SafeExamBrowser.Runtime.Behaviour
public void Terminate()
{
DeregisterEvents();
if (sessionRunning)
{
DeregisterSessionEvents();
StopSession();
}
@ -119,6 +124,8 @@ namespace SafeExamBrowser.Runtime.Behaviour
{
logger.Info("--- Shutdown procedure failed! ---");
logger.Log(string.Empty);
uiFactory.Show(TextKey.MessageBox_ShutdownError, TextKey.MessageBox_ShutdownErrorTitle, icon: MessageBoxIcon.Error);
}
splashScreen?.Close();
@ -127,13 +134,20 @@ namespace SafeExamBrowser.Runtime.Behaviour
private void StartSession(bool initial = false)
{
runtimeWindow.Show();
logger.Info(">------ Initiating session procedure ------<");
logger.Info(">>>--- Initiating session procedure ---<<<");
if (sessionRunning)
{
DeregisterSessionEvents();
}
sessionRunning = initial ? sessionSequence.TryPerform() : sessionSequence.TryRepeat();
if (sessionRunning)
{
logger.Info(">------ Session is running ------<");
RegisterSessionEvents();
logger.Info(">>>--- Session is running ---<<<");
runtimeWindow.HideProgressBar();
runtimeWindow.UpdateText(TextKey.RuntimeWindow_ApplicationRunning);
@ -144,7 +158,7 @@ namespace SafeExamBrowser.Runtime.Behaviour
}
else
{
logger.Info(">------ Session procedure was aborted! ------<");
logger.Info(">>>--- Session procedure was aborted! ---<<<");
// TODO: Not when user chose to terminate after reconfiguration! Probably needs IOperationSequenceResult or alike...
uiFactory.Show(TextKey.MessageBox_SessionStartError, TextKey.MessageBox_SessionStartErrorTitle, icon: MessageBoxIcon.Error);
@ -160,28 +174,65 @@ namespace SafeExamBrowser.Runtime.Behaviour
runtimeWindow.Show();
runtimeWindow.BringToForeground();
runtimeWindow.ShowProgressBar();
logger.Info(">------ Reverting session operations ------<");
logger.Info(">>>--- Reverting session operations ---<<<");
var success = sessionSequence.TryRevert();
if (success)
{
logger.Info(">------ Session is terminated ------<");
logger.Info(">>>--- Session is terminated ---<<<");
sessionRunning = false;
// TODO
}
else
{
logger.Info(">------ Session reversion was erroneous! ------<");
// TODO
logger.Info(">>>--- Session reversion was erroneous! ---<<<");
uiFactory.Show(TextKey.MessageBox_SessionStopError, TextKey.MessageBox_SessionStopErrorTitle, icon: MessageBoxIcon.Error);
}
}
private void RegisterEvents()
{
client.ConnectionLost += Client_ConnectionLost;
runtimeHost.ShutdownRequested += RuntimeHost_ShutdownRequested;
}
private void RegisterSessionEvents()
{
configuration.CurrentSession.ClientProcess.Terminated += ClientProcess_Terminated;
}
private void DeregisterEvents()
{
client.ConnectionLost -= Client_ConnectionLost;
runtimeHost.ShutdownRequested -= RuntimeHost_ShutdownRequested;
}
private void DeregisterSessionEvents()
{
configuration.CurrentSession.ClientProcess.Terminated -= ClientProcess_Terminated;
}
private void ClientProcess_Terminated(int exitCode)
{
logger.Error($"Client application has unexpectedly terminated with exit code {exitCode}!");
// TODO: Check if message box is rendered on new desktop as well -> otherwise shutdown is blocked!
uiFactory.Show(TextKey.MessageBox_ApplicationError, TextKey.MessageBox_ApplicationErrorTitle, icon: MessageBoxIcon.Error);
shutdown.Invoke();
}
private void Client_ConnectionLost()
{
logger.Error("Lost connection to the client application!");
// TODO: Check if message box is rendered on new desktop as well -> otherwise shutdown is blocked!
uiFactory.Show(TextKey.MessageBox_ApplicationError, TextKey.MessageBox_ApplicationErrorTitle, icon: MessageBoxIcon.Error);
shutdown.Invoke();
}
private void RuntimeHost_ShutdownRequested()
{
logger.Info("Received shutdown request from client application.");
logger.Info("Received shutdown request from the client application.");
shutdown.Invoke();
}
}

View file

@ -18,6 +18,7 @@ namespace SafeExamBrowser.Runtime.Communication
{
internal class RuntimeHost : BaseHost, IRuntimeHost
{
private bool allowConnection = true;
private IConfigurationRepository configuration;
public Guid StartupToken { private get; set; }
@ -33,7 +34,15 @@ namespace SafeExamBrowser.Runtime.Communication
protected override bool OnConnect(Guid? token = null)
{
return StartupToken == token;
var authenticated = StartupToken == token;
var accepted = allowConnection && authenticated;
if (accepted)
{
allowConnection = false;
}
return accepted;
}
protected override void OnDisconnect()
@ -43,7 +52,6 @@ namespace SafeExamBrowser.Runtime.Communication
protected override Response OnReceive(Message message)
{
// TODO
return new SimpleResponse(SimpleResponsePurport.UnknownMessage);
}