diff --git a/SafeExamBrowser.Client/Behaviour/ClientController.cs b/SafeExamBrowser.Client/Behaviour/ClientController.cs
index 8453c6d8..492fbd52 100644
--- a/SafeExamBrowser.Client/Behaviour/ClientController.cs
+++ b/SafeExamBrowser.Client/Behaviour/ClientController.cs
@@ -26,6 +26,8 @@ namespace SafeExamBrowser.Client.Behaviour
private ITaskbar taskbar;
private IWindowMonitor windowMonitor;
+ public IClientHost ClientHost { private get; set; }
+
public ClientController(
IDisplayMonitor displayMonitor,
ILogger logger,
@@ -44,34 +46,46 @@ namespace SafeExamBrowser.Client.Behaviour
this.windowMonitor = windowMonitor;
}
+ public bool TryStart()
+ {
+ var success = operations.TryPerform();
+
+ // TODO
+
+ if (success)
+ {
+ RegisterEvents();
+ runtime.InformClientReady();
+ }
+
+ return success;
+ }
+
public void Terminate()
{
- displayMonitor.DisplayChanged -= DisplayMonitor_DisplaySettingsChanged;
- processMonitor.ExplorerStarted -= ProcessMonitor_ExplorerStarted;
- windowMonitor.WindowChanged -= WindowMonitor_WindowChanged;
+ DeregisterEvents();
// TODO
operations.TryRevert();
}
- public bool TryStart()
+ private void RegisterEvents()
{
+ ClientHost.Shutdown += ClientHost_Shutdown;
+ displayMonitor.DisplayChanged += DisplayMonitor_DisplaySettingsChanged;
+ processMonitor.ExplorerStarted += ProcessMonitor_ExplorerStarted;
+ taskbar.QuitButtonClicked += Taskbar_QuitButtonClicked;
+ windowMonitor.WindowChanged += WindowMonitor_WindowChanged;
+ }
- // TODO
-
- var success = operations.TryPerform();
-
- if (success)
- {
- displayMonitor.DisplayChanged += DisplayMonitor_DisplaySettingsChanged;
- processMonitor.ExplorerStarted += ProcessMonitor_ExplorerStarted;
- windowMonitor.WindowChanged += WindowMonitor_WindowChanged;
-
- runtime.InformClientReady();
- }
-
- return success;
+ private void DeregisterEvents()
+ {
+ ClientHost.Shutdown -= ClientHost_Shutdown;
+ displayMonitor.DisplayChanged -= DisplayMonitor_DisplaySettingsChanged;
+ processMonitor.ExplorerStarted -= ProcessMonitor_ExplorerStarted;
+ taskbar.QuitButtonClicked -= Taskbar_QuitButtonClicked;
+ windowMonitor.WindowChanged -= WindowMonitor_WindowChanged;
}
private void DisplayMonitor_DisplaySettingsChanged()
@@ -94,6 +108,24 @@ namespace SafeExamBrowser.Client.Behaviour
logger.Info("Desktop successfully restored.");
}
+ private void ClientHost_Shutdown()
+ {
+ // TODO: Better use callback to Application.Shutdown() as in runtime?
+ taskbar.Close();
+ }
+
+ private void Taskbar_QuitButtonClicked()
+ {
+ // TODO: MessageBox asking whether user really wants to quit -> args.Cancel
+
+ var acknowledged = runtime.RequestShutdown();
+
+ if (!acknowledged)
+ {
+ logger.Warn("The runtime did not acknowledge the shutdown request!");
+ }
+ }
+
private void WindowMonitor_WindowChanged(IntPtr window)
{
var allowed = processMonitor.BelongsToAllowedProcess(window);
diff --git a/SafeExamBrowser.Client/Communication/ClientHost.cs b/SafeExamBrowser.Client/Communication/ClientHost.cs
index c89aeb7f..fdcbedf2 100644
--- a/SafeExamBrowser.Client/Communication/ClientHost.cs
+++ b/SafeExamBrowser.Client/Communication/ClientHost.cs
@@ -21,6 +21,8 @@ namespace SafeExamBrowser.Client.Communication
public Guid StartupToken { private get; set; }
+ public event CommunicationEventHandler Shutdown;
+
public ClientHost(string address, ILogger logger, int processId) : base(address, logger)
{
this.processId = processId;
@@ -49,6 +51,9 @@ namespace SafeExamBrowser.Client.Communication
{
case SimpleMessagePurport.Authenticate:
return new AuthenticationResponse { ProcessId = processId };
+ case SimpleMessagePurport.Shutdown:
+ Shutdown?.Invoke();
+ return new SimpleResponse(SimpleResponsePurport.Acknowledged);
}
return new SimpleResponse(SimpleResponsePurport.UnknownMessage);
diff --git a/SafeExamBrowser.Client/CompositionRoot.cs b/SafeExamBrowser.Client/CompositionRoot.cs
index 70f11bec..e36895a4 100644
--- a/SafeExamBrowser.Client/CompositionRoot.cs
+++ b/SafeExamBrowser.Client/CompositionRoot.cs
@@ -102,6 +102,7 @@ namespace SafeExamBrowser.Client
internal void LogShutdownInformation()
{
+ logger?.Log(string.Empty);
logger?.Log($"# Client instance terminated at {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
}
@@ -153,6 +154,7 @@ namespace SafeExamBrowser.Client
var operation = new CommunicationOperation(host, logger);
host.StartupToken = startupToken;
+ ClientController.ClientHost = host;
return operation;
}
diff --git a/SafeExamBrowser.Configuration/ConfigurationRepository.cs b/SafeExamBrowser.Configuration/ConfigurationRepository.cs
index 194a3552..bcc23def 100644
--- a/SafeExamBrowser.Configuration/ConfigurationRepository.cs
+++ b/SafeExamBrowser.Configuration/ConfigurationRepository.cs
@@ -73,6 +73,12 @@ namespace SafeExamBrowser.Configuration
};
settings.Browser.StartUrl = "https://www.duckduckgo.com";
+ settings.Browser.AllowAddressBar = true;
+ settings.Browser.AllowBackwardNavigation = true;
+ settings.Browser.AllowDeveloperConsole = true;
+ settings.Browser.AllowForwardNavigation = true;
+ settings.Browser.AllowReloading = true;
+
settings.Taskbar.AllowApplicationLog = true;
settings.Taskbar.AllowKeyboardLayout = true;
settings.Taskbar.AllowWirelessNetwork = true;
diff --git a/SafeExamBrowser.Contracts/Behaviour/IClientController.cs b/SafeExamBrowser.Contracts/Behaviour/IClientController.cs
index 02a10bd2..208a38a3 100644
--- a/SafeExamBrowser.Contracts/Behaviour/IClientController.cs
+++ b/SafeExamBrowser.Contracts/Behaviour/IClientController.cs
@@ -6,11 +6,18 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
+using SafeExamBrowser.Contracts.Communication;
+
namespace SafeExamBrowser.Contracts.Behaviour
{
// TODO: Interface really needed?!
public interface IClientController
{
+ ///
+ /// The client host used for communication handling.
+ ///
+ IClientHost ClientHost { set; }
+
///
/// Reverts any changes, releases all used resources and terminates the client.
///
diff --git a/SafeExamBrowser.Contracts/Communication/IClientHost.cs b/SafeExamBrowser.Contracts/Communication/IClientHost.cs
index e83846a5..4e7b7e7f 100644
--- a/SafeExamBrowser.Contracts/Communication/IClientHost.cs
+++ b/SafeExamBrowser.Contracts/Communication/IClientHost.cs
@@ -16,5 +16,10 @@ namespace SafeExamBrowser.Contracts.Communication
/// The startup token used for initial authentication.
///
Guid StartupToken { set; }
+
+ ///
+ /// Event fired when the runtime commands the client to shutdown.
+ ///
+ event CommunicationEventHandler Shutdown;
}
}
diff --git a/SafeExamBrowser.Contracts/Communication/IClientProxy.cs b/SafeExamBrowser.Contracts/Communication/IClientProxy.cs
index 3585f249..13698163 100644
--- a/SafeExamBrowser.Contracts/Communication/IClientProxy.cs
+++ b/SafeExamBrowser.Contracts/Communication/IClientProxy.cs
@@ -12,6 +12,11 @@ namespace SafeExamBrowser.Contracts.Communication
{
public interface IClientProxy : ICommunicationProxy
{
+ ///
+ /// Instructs the client to initiate its shutdown procedure.
+ ///
+ void InitiateShutdown();
+
///
/// Instructs the client to submit its authentication data.
///
diff --git a/SafeExamBrowser.Contracts/Communication/IRuntimeHost.cs b/SafeExamBrowser.Contracts/Communication/IRuntimeHost.cs
index 7e3be964..6aa636f9 100644
--- a/SafeExamBrowser.Contracts/Communication/IRuntimeHost.cs
+++ b/SafeExamBrowser.Contracts/Communication/IRuntimeHost.cs
@@ -17,9 +17,19 @@ namespace SafeExamBrowser.Contracts.Communication
///
Guid StartupToken { set; }
+ ///
+ /// Event fired when the client disconnected from the runtime.
+ ///
+ event CommunicationEventHandler ClientDisconnected;
+
///
/// Event fired once the client has signaled that it is ready to operate.
///
event CommunicationEventHandler ClientReady;
+
+ ///
+ /// Event fired when the client requests to shut down the application.
+ ///
+ event CommunicationEventHandler ShutdownRequested;
}
}
diff --git a/SafeExamBrowser.Contracts/Communication/IRuntimeProxy.cs b/SafeExamBrowser.Contracts/Communication/IRuntimeProxy.cs
index 4405486f..2f674385 100644
--- a/SafeExamBrowser.Contracts/Communication/IRuntimeProxy.cs
+++ b/SafeExamBrowser.Contracts/Communication/IRuntimeProxy.cs
@@ -15,11 +15,18 @@ namespace SafeExamBrowser.Contracts.Communication
///
/// Retrieves the application configuration from the runtime.
///
+ /// If the configuration could not be retrieved.
ClientConfiguration GetConfiguration();
///
/// Informs the runtime that the client is ready.
///
+ /// If the runtime did not acknowledge the status update.
void InformClientReady();
+
+ ///
+ /// Requests the runtime to shut down the application. Returns true if the request was acknowledged, otherwise false.
+ ///
+ bool RequestShutdown();
}
}
diff --git a/SafeExamBrowser.Contracts/Communication/Messages/SimpleMessagePurport.cs b/SafeExamBrowser.Contracts/Communication/Messages/SimpleMessagePurport.cs
index e4c6f4a6..79a266ec 100644
--- a/SafeExamBrowser.Contracts/Communication/Messages/SimpleMessagePurport.cs
+++ b/SafeExamBrowser.Contracts/Communication/Messages/SimpleMessagePurport.cs
@@ -27,5 +27,15 @@ namespace SafeExamBrowser.Contracts.Communication.Messages
/// Sent from the client to the runtime to ask for the client configuration.
///
ConfigurationNeeded,
+
+ ///
+ /// Sent from the client to the runtime to request shutting down the application.
+ ///
+ RequestShutdown,
+
+ ///
+ /// Sent form the runtime to the client to command the latter to shut itself down.
+ ///
+ Shutdown
}
}
diff --git a/SafeExamBrowser.Contracts/UserInterface/Taskbar/ITaskbar.cs b/SafeExamBrowser.Contracts/UserInterface/Taskbar/ITaskbar.cs
index c2ffc90c..5ee04a47 100644
--- a/SafeExamBrowser.Contracts/UserInterface/Taskbar/ITaskbar.cs
+++ b/SafeExamBrowser.Contracts/UserInterface/Taskbar/ITaskbar.cs
@@ -8,8 +8,15 @@
namespace SafeExamBrowser.Contracts.UserInterface.Taskbar
{
+ public delegate void QuitButtonClickedEventHandler();
+
public interface ITaskbar
{
+ ///
+ /// Event fired when the user clicked the quit button in the taskbar.
+ ///
+ event QuitButtonClickedEventHandler QuitButtonClicked;
+
///
/// Adds the given application button to the taskbar.
///
@@ -25,6 +32,11 @@ namespace SafeExamBrowser.Contracts.UserInterface.Taskbar
///
void AddSystemControl(ISystemControl control);
+ ///
+ /// Closes the taskbar.
+ ///
+ void Close();
+
///
/// Returns the absolute height of the taskbar (i.e. in physical pixels).
///
diff --git a/SafeExamBrowser.Contracts/WindowsApi/IProcess.cs b/SafeExamBrowser.Contracts/WindowsApi/IProcess.cs
index 11abb31d..2b310acb 100644
--- a/SafeExamBrowser.Contracts/WindowsApi/IProcess.cs
+++ b/SafeExamBrowser.Contracts/WindowsApi/IProcess.cs
@@ -8,11 +8,28 @@
namespace SafeExamBrowser.Contracts.WindowsApi
{
+ public delegate void ProcessTerminatedEventHandler(int exitCode);
+
public interface IProcess
{
+ ///
+ /// Indicates whether the process has been terminated.
+ ///
+ bool HasTerminated { get; }
+
///
/// The process identifier.
///
int Id { get; }
+
+ ///
+ /// Event fired when the process has terminated.
+ ///
+ event ProcessTerminatedEventHandler Terminated;
+
+ ///
+ /// Immediately terminates the process.
+ ///
+ void Kill();
}
}
diff --git a/SafeExamBrowser.Core/Communication/BaseHost.cs b/SafeExamBrowser.Core/Communication/BaseHost.cs
index 1e9136b3..bdd64f51 100644
--- a/SafeExamBrowser.Core/Communication/BaseHost.cs
+++ b/SafeExamBrowser.Core/Communication/BaseHost.cs
@@ -97,9 +97,7 @@ namespace SafeExamBrowser.Core.Communication
{
lock (@lock)
{
- Response response = null;
-
- logger.Debug($"Received message '{ToString(message)}'.");
+ var response = default(Response);
if (IsAuthorized(message?.CommunicationToken))
{
@@ -113,7 +111,7 @@ namespace SafeExamBrowser.Core.Communication
}
}
- logger.Debug($"Sending response '{ToString(response)}'.");
+ logger.Debug($"Received message '{ToString(message)}', sending response '{ToString(response)}'.");
return response;
}
diff --git a/SafeExamBrowser.Core/Communication/BaseProxy.cs b/SafeExamBrowser.Core/Communication/BaseProxy.cs
index c46a4977..40ef9772 100644
--- a/SafeExamBrowser.Core/Communication/BaseProxy.cs
+++ b/SafeExamBrowser.Core/Communication/BaseProxy.cs
@@ -80,6 +80,16 @@ namespace SafeExamBrowser.Core.Communication
return Send(new SimpleMessage(purport));
}
+ protected string ToString(Message message)
+ {
+ return message != null ? message.ToString() : "";
+ }
+
+ protected string ToString(Response response)
+ {
+ return response != null ? response.ToString() : "";
+ }
+
private void BaseProxy_Closed(object sender, EventArgs e)
{
Logger.Debug("Communication channel has been closed.");
@@ -122,15 +132,5 @@ namespace SafeExamBrowser.Core.Communication
{
return channel == null ? "null" : $"in state '{(channel as ICommunicationObject).State}'";
}
-
- private string ToString(Message message)
- {
- return message != null ? message.ToString() : "";
- }
-
- private string ToString(Response response)
- {
- return response != null ? response.ToString() : "";
- }
}
}
diff --git a/SafeExamBrowser.Core/Communication/ClientProxy.cs b/SafeExamBrowser.Core/Communication/ClientProxy.cs
index 4bfe6d36..5009463b 100644
--- a/SafeExamBrowser.Core/Communication/ClientProxy.cs
+++ b/SafeExamBrowser.Core/Communication/ClientProxy.cs
@@ -19,6 +19,16 @@ namespace SafeExamBrowser.Core.Communication
{
}
+ public void InitiateShutdown()
+ {
+ var response = Send(SimpleMessagePurport.Shutdown);
+
+ if (response is SimpleResponse simpleMessage && simpleMessage.Purport == SimpleResponsePurport.Acknowledged)
+ {
+ // TODO
+ }
+ }
+
public AuthenticationResponse RequestAuthentication()
{
var response = Send(SimpleMessagePurport.Authenticate);
diff --git a/SafeExamBrowser.Core/Communication/RuntimeProxy.cs b/SafeExamBrowser.Core/Communication/RuntimeProxy.cs
index f66b7e01..16b445b7 100644
--- a/SafeExamBrowser.Core/Communication/RuntimeProxy.cs
+++ b/SafeExamBrowser.Core/Communication/RuntimeProxy.cs
@@ -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;
@@ -22,12 +23,37 @@ namespace SafeExamBrowser.Core.Communication
public ClientConfiguration GetConfiguration()
{
- return ((ConfigurationResponse) Send(SimpleMessagePurport.ConfigurationNeeded)).Configuration;
+ var response = Send(SimpleMessagePurport.ConfigurationNeeded);
+
+ if (response is ConfigurationResponse configurationResponse)
+ {
+ return configurationResponse.Configuration;
+ }
+
+ throw new CommunicationException($"Could not retrieve client configuration! Received: {ToString(response)}.");
}
public void InformClientReady()
{
- Send(SimpleMessagePurport.ClientIsReady);
+ var response = Send(SimpleMessagePurport.ClientIsReady);
+
+ if (!IsAcknowledgeResponse(response))
+ {
+ throw new CommunicationException($"Runtime did not acknowledge that client is ready! Response: {ToString(response)}.");
+ }
+ }
+
+ public bool RequestShutdown()
+ {
+ var response = Send(SimpleMessagePurport.RequestShutdown);
+ var acknowledged = IsAcknowledgeResponse(response);
+
+ return acknowledged;
+ }
+
+ private bool IsAcknowledgeResponse(Response response)
+ {
+ return response is SimpleResponse simpleResponse && simpleResponse.Purport == SimpleResponsePurport.Acknowledged;
}
}
}
diff --git a/SafeExamBrowser.Core/Communication/ServiceProxy.cs b/SafeExamBrowser.Core/Communication/ServiceProxy.cs
index 8e45a2e4..74140c99 100644
--- a/SafeExamBrowser.Core/Communication/ServiceProxy.cs
+++ b/SafeExamBrowser.Core/Communication/ServiceProxy.cs
@@ -58,7 +58,7 @@ namespace SafeExamBrowser.Core.Communication
return;
}
- // TODO
+ // TODO: Send(new StopSessionMessage { SessionId = sessionId });
}
private bool IgnoreOperation(string operationName)
diff --git a/SafeExamBrowser.Runtime/Behaviour/Operations/SessionSequenceOperation.cs b/SafeExamBrowser.Runtime/Behaviour/Operations/SessionSequenceOperation.cs
index 7d12ebf8..f29da2a1 100644
--- a/SafeExamBrowser.Runtime/Behaviour/Operations/SessionSequenceOperation.cs
+++ b/SafeExamBrowser.Runtime/Behaviour/Operations/SessionSequenceOperation.cs
@@ -20,6 +20,8 @@ namespace SafeExamBrowser.Runtime.Behaviour.Operations
{
internal abstract class SessionSequenceOperation : IOperation
{
+ private const int TEN_SECONDS = 10000;
+
private bool sessionRunning;
private IClientProxy client;
private IConfigurationRepository configuration;
@@ -68,7 +70,6 @@ namespace SafeExamBrowser.Runtime.Behaviour.Operations
}
catch (Exception e)
{
- service.StopSession(session.Id);
logger.Error("Failed to start client!", e);
}
@@ -80,6 +81,7 @@ namespace SafeExamBrowser.Runtime.Behaviour.Operations
{
Abort = true;
logger.Info($"Failed to start new session! Aborting...");
+ service.StopSession(session.Id);
}
}
@@ -92,8 +94,14 @@ namespace SafeExamBrowser.Runtime.Behaviour.Operations
service.StopSession(session.Id);
- // TODO:
- // - Terminate client (or does it terminate itself?)
+ try
+ {
+ StopClient();
+ }
+ catch (Exception e)
+ {
+ logger.Error("Failed to terminate client!", e);
+ }
sessionRunning = false;
logger.Info($"Successfully stopped session with identifier '{session.Id}'.");
@@ -102,9 +110,7 @@ namespace SafeExamBrowser.Runtime.Behaviour.Operations
private void StartClient()
{
- const int TEN_SECONDS = 10000;
-
- var clientStarted = false;
+ var clientReady = false;
var clientReadyEvent = new AutoResetEvent(false);
var clientReadyEventHandler = new CommunicationEventHandler(() => clientReadyEvent.Set());
var clientExecutable = configuration.RuntimeInfo.ClientExecutablePath;
@@ -113,13 +119,14 @@ namespace SafeExamBrowser.Runtime.Behaviour.Operations
var token = session.StartupToken.ToString("D");
runtimeHost.ClientReady += clientReadyEventHandler;
- session.ClientProcess = processFactory.StartNew(clientExecutable, clientLogFile, hostUri, token);
- clientStarted = clientReadyEvent.WaitOne(TEN_SECONDS);
+ session.ClientProcess = processFactory.StartNew(clientExecutable, clientLogFile, hostUri, token);
+ clientReady = clientReadyEvent.WaitOne(TEN_SECONDS);
+
runtimeHost.ClientReady -= clientReadyEventHandler;
// TODO: Check if client process alive!
- if (clientStarted)
+ if (clientReady)
{
if (client.Connect(session.StartupToken))
{
@@ -145,5 +152,71 @@ namespace SafeExamBrowser.Runtime.Behaviour.Operations
logger.Error($"Failed to start client within {TEN_SECONDS / 1000} seconds!");
}
}
+
+ private void StopClient()
+ {
+ var disconnected = false;
+ var disconnectedEvent = new AutoResetEvent(false);
+ var disconnectedEventHandler = new CommunicationEventHandler(() => disconnectedEvent.Set());
+
+ var terminated = false;
+ var terminatedEvent = new AutoResetEvent(false);
+ var terminatedEventHandler = new ProcessTerminatedEventHandler((_) => terminatedEvent.Set());
+
+ runtimeHost.ClientDisconnected += disconnectedEventHandler;
+ session.ClientProcess.Terminated += terminatedEventHandler;
+
+ client.InitiateShutdown();
+ client.Disconnect();
+
+ disconnected = disconnectedEvent.WaitOne(TEN_SECONDS);
+ terminated = terminatedEvent.WaitOne(TEN_SECONDS);
+
+ runtimeHost.ClientDisconnected -= disconnectedEventHandler;
+ session.ClientProcess.Terminated -= terminatedEventHandler;
+
+ if (disconnected && terminated)
+ {
+ logger.Info("Client has been successfully terminated.");
+ }
+ else
+ {
+ if (!disconnected)
+ {
+ logger.Error($"Client failed to disconnect within {TEN_SECONDS / 1000} seconds!");
+ }
+ else if (!terminated)
+ {
+ logger.Error($"Client failed to terminate within {TEN_SECONDS / 1000} seconds!");
+ }
+
+ KillClient();
+ }
+ }
+
+ private void KillClient(int attempt = 0)
+ {
+ const int MAX_ATTEMPTS = 5;
+
+ if (attempt == MAX_ATTEMPTS)
+ {
+ logger.Error($"Failed to kill client process within {MAX_ATTEMPTS} attempts!");
+
+ return;
+ }
+
+ logger.Info($"Killing client process with ID = {session.ClientProcess.Id}.");
+ session.ClientProcess.Kill();
+
+ if (session.ClientProcess.HasTerminated)
+ {
+ logger.Info("Client process has terminated.");
+ }
+ else
+ {
+ logger.Warn("Failed to kill client process. Trying again...");
+ KillClient(attempt++);
+ }
+ }
}
}
diff --git a/SafeExamBrowser.Runtime/Behaviour/RuntimeController.cs b/SafeExamBrowser.Runtime/Behaviour/RuntimeController.cs
index 2d78bbbb..31a57257 100644
--- a/SafeExamBrowser.Runtime/Behaviour/RuntimeController.cs
+++ b/SafeExamBrowser.Runtime/Behaviour/RuntimeController.cs
@@ -80,6 +80,7 @@ namespace SafeExamBrowser.Runtime.Behaviour
logger.Subscribe(runtimeWindow);
splashScreen.Hide();
+ runtimeHost.ShutdownRequested += RuntimeHost_ShutdownRequested;
StartSession(true);
}
@@ -169,5 +170,11 @@ namespace SafeExamBrowser.Runtime.Behaviour
// TODO
}
}
+
+ private void RuntimeHost_ShutdownRequested()
+ {
+ logger.Info("Received shutdown request from client application.");
+ shutdown.Invoke();
+ }
}
}
diff --git a/SafeExamBrowser.Runtime/Communication/RuntimeHost.cs b/SafeExamBrowser.Runtime/Communication/RuntimeHost.cs
index 9b810395..abd0768d 100644
--- a/SafeExamBrowser.Runtime/Communication/RuntimeHost.cs
+++ b/SafeExamBrowser.Runtime/Communication/RuntimeHost.cs
@@ -7,6 +7,7 @@
*/
using System;
+using System.Threading.Tasks;
using SafeExamBrowser.Contracts.Communication;
using SafeExamBrowser.Contracts.Communication.Messages;
using SafeExamBrowser.Contracts.Communication.Responses;
@@ -22,7 +23,9 @@ namespace SafeExamBrowser.Runtime.Communication
public Guid StartupToken { private get; set; }
+ public event CommunicationEventHandler ClientDisconnected;
public event CommunicationEventHandler ClientReady;
+ public event CommunicationEventHandler ShutdownRequested;
public RuntimeHost(string address, IConfigurationRepository configuration, ILogger logger) : base(address, logger)
{
@@ -36,7 +39,7 @@ namespace SafeExamBrowser.Runtime.Communication
protected override void OnDisconnect()
{
- // TODO
+ Task.Run(() => ClientDisconnected?.Invoke());
}
protected override Response OnReceive(Message message)
@@ -54,6 +57,9 @@ namespace SafeExamBrowser.Runtime.Communication
return new SimpleResponse(SimpleResponsePurport.Acknowledged);
case SimpleMessagePurport.ConfigurationNeeded:
return new ConfigurationResponse { Configuration = configuration.BuildClientConfiguration() };
+ case SimpleMessagePurport.RequestShutdown:
+ ShutdownRequested?.Invoke();
+ return new SimpleResponse(SimpleResponsePurport.Acknowledged);
}
return new SimpleResponse(SimpleResponsePurport.UnknownMessage);
diff --git a/SafeExamBrowser.Runtime/CompositionRoot.cs b/SafeExamBrowser.Runtime/CompositionRoot.cs
index 4eb5c1df..cdb521b1 100644
--- a/SafeExamBrowser.Runtime/CompositionRoot.cs
+++ b/SafeExamBrowser.Runtime/CompositionRoot.cs
@@ -39,7 +39,7 @@ namespace SafeExamBrowser.Runtime
var args = Environment.GetCommandLineArgs();
var configuration = new ConfigurationRepository();
var nativeMethods = new NativeMethods();
- Action shutdown = Application.Current.Shutdown;
+ void shutdown() => Application.Current.Dispatcher.BeginInvoke(new Action(Application.Current.Shutdown));
logger = new Logger();
runtimeInfo = configuration.RuntimeInfo;
@@ -75,12 +75,10 @@ namespace SafeExamBrowser.Runtime
internal void LogStartupInformation()
{
- var titleLine = $"/* {runtimeInfo.ProgramTitle}, Version {runtimeInfo.ProgramVersion}{Environment.NewLine}";
- var copyrightLine = $"/* {runtimeInfo.ProgramCopyright}{Environment.NewLine}";
- var emptyLine = $"/* {Environment.NewLine}";
- var githubLine = $"/* Please visit https://www.github.com/SafeExamBrowser for more information.";
-
- logger.Log($"{titleLine}{copyrightLine}{emptyLine}{githubLine}");
+ logger.Log($"/* {runtimeInfo.ProgramTitle}, Version {runtimeInfo.ProgramVersion}");
+ logger.Log($"/* {runtimeInfo.ProgramCopyright}");
+ logger.Log($"/* ");
+ logger.Log($"/* Please visit https://www.github.com/SafeExamBrowser for more information.");
logger.Log(string.Empty);
logger.Log($"# Application started at {runtimeInfo.ApplicationStartTime.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
logger.Log($"# Running on {systemInfo.OperatingSystemInfo}");
diff --git a/SafeExamBrowser.UserInterface.Classic/Controls/QuitButton.xaml.cs b/SafeExamBrowser.UserInterface.Classic/Controls/QuitButton.xaml.cs
index f11dc852..b9e4f3c6 100644
--- a/SafeExamBrowser.UserInterface.Classic/Controls/QuitButton.xaml.cs
+++ b/SafeExamBrowser.UserInterface.Classic/Controls/QuitButton.xaml.cs
@@ -9,12 +9,15 @@
using System;
using System.Windows;
using System.Windows.Controls;
+using SafeExamBrowser.Contracts.UserInterface.Taskbar;
using SafeExamBrowser.UserInterface.Classic.Utilities;
namespace SafeExamBrowser.UserInterface.Classic.Controls
{
public partial class QuitButton : UserControl
{
+ public QuitButtonClickedEventHandler Clicked;
+
public QuitButton()
{
InitializeComponent();
@@ -23,7 +26,7 @@ namespace SafeExamBrowser.UserInterface.Classic.Controls
private void Button_Click(object sender, RoutedEventArgs e)
{
- Application.Current.MainWindow.Close();
+ Clicked?.Invoke();
}
private void LoadIcon()
diff --git a/SafeExamBrowser.UserInterface.Classic/Taskbar.xaml b/SafeExamBrowser.UserInterface.Classic/Taskbar.xaml
index 3ba24fd9..17de6259 100644
--- a/SafeExamBrowser.UserInterface.Classic/Taskbar.xaml
+++ b/SafeExamBrowser.UserInterface.Classic/Taskbar.xaml
@@ -25,6 +25,6 @@
-
+
diff --git a/SafeExamBrowser.UserInterface.Classic/Taskbar.xaml.cs b/SafeExamBrowser.UserInterface.Classic/Taskbar.xaml.cs
index 81f92ddd..cbaded3e 100644
--- a/SafeExamBrowser.UserInterface.Classic/Taskbar.xaml.cs
+++ b/SafeExamBrowser.UserInterface.Classic/Taskbar.xaml.cs
@@ -6,6 +6,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
+using System.ComponentModel;
using System.Windows;
using SafeExamBrowser.Contracts.Logging;
using SafeExamBrowser.Contracts.UserInterface.Taskbar;
@@ -17,14 +18,17 @@ namespace SafeExamBrowser.UserInterface.Classic
{
private ILogger logger;
+ public event QuitButtonClickedEventHandler QuitButtonClicked;
+
public Taskbar(ILogger logger)
{
InitializeComponent();
this.logger = logger;
- Loaded += (o, args) => InitializeBounds();
Closing += Taskbar_Closing;
+ Loaded += (o, args) => InitializeBounds();
+ QuitButton.Clicked += () => QuitButtonClicked?.Invoke();
}
public void AddApplication(IApplicationButton button)
@@ -51,6 +55,11 @@ namespace SafeExamBrowser.UserInterface.Classic
}
}
+ public new void Close()
+ {
+ Dispatcher.Invoke(base.Close);
+ }
+
public int GetAbsoluteHeight()
{
return Dispatcher.Invoke(() =>
@@ -78,7 +87,7 @@ namespace SafeExamBrowser.UserInterface.Classic
});
}
- private void Taskbar_Closing(object sender, System.ComponentModel.CancelEventArgs e)
+ private void Taskbar_Closing(object sender, CancelEventArgs e)
{
foreach (var child in SystemControlStackPanel.Children)
{
diff --git a/SafeExamBrowser.UserInterface.Windows10/Controls/QuitButton.xaml.cs b/SafeExamBrowser.UserInterface.Windows10/Controls/QuitButton.xaml.cs
index 26a2b6a4..dacf1717 100644
--- a/SafeExamBrowser.UserInterface.Windows10/Controls/QuitButton.xaml.cs
+++ b/SafeExamBrowser.UserInterface.Windows10/Controls/QuitButton.xaml.cs
@@ -8,11 +8,14 @@
using System.Windows;
using System.Windows.Controls;
+using SafeExamBrowser.Contracts.UserInterface.Taskbar;
namespace SafeExamBrowser.UserInterface.Windows10.Controls
{
public partial class QuitButton : UserControl
{
+ public event QuitButtonClickedEventHandler Clicked;
+
public QuitButton()
{
InitializeComponent();
@@ -20,7 +23,7 @@ namespace SafeExamBrowser.UserInterface.Windows10.Controls
private void Button_Click(object sender, RoutedEventArgs e)
{
- Application.Current.MainWindow.Close();
+ Clicked?.Invoke();
}
}
}
diff --git a/SafeExamBrowser.UserInterface.Windows10/Taskbar.xaml b/SafeExamBrowser.UserInterface.Windows10/Taskbar.xaml
index 44bf2ad2..607cd586 100644
--- a/SafeExamBrowser.UserInterface.Windows10/Taskbar.xaml
+++ b/SafeExamBrowser.UserInterface.Windows10/Taskbar.xaml
@@ -29,7 +29,7 @@
-
+
diff --git a/SafeExamBrowser.UserInterface.Windows10/Taskbar.xaml.cs b/SafeExamBrowser.UserInterface.Windows10/Taskbar.xaml.cs
index dcf739b4..72e3ed3f 100644
--- a/SafeExamBrowser.UserInterface.Windows10/Taskbar.xaml.cs
+++ b/SafeExamBrowser.UserInterface.Windows10/Taskbar.xaml.cs
@@ -6,6 +6,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
+using System.ComponentModel;
using System.Windows;
using SafeExamBrowser.Contracts.Logging;
using SafeExamBrowser.Contracts.UserInterface.Taskbar;
@@ -17,6 +18,8 @@ namespace SafeExamBrowser.UserInterface.Windows10
{
private ILogger logger;
+ public event QuitButtonClickedEventHandler QuitButtonClicked;
+
public Taskbar(ILogger logger)
{
this.logger = logger;
@@ -25,6 +28,7 @@ namespace SafeExamBrowser.UserInterface.Windows10
Loaded += (o, args) => InitializeBounds();
Closing += Taskbar_Closing;
+ QuitButtonClicked += Taskbar_QuitButtonClicked;
}
public void AddApplication(IApplicationButton button)
@@ -51,6 +55,11 @@ namespace SafeExamBrowser.UserInterface.Windows10
}
}
+ public new void Close()
+ {
+ Dispatcher.Invoke(base.Close);
+ }
+
public int GetAbsoluteHeight()
{
return Dispatcher.Invoke(() =>
@@ -78,7 +87,12 @@ namespace SafeExamBrowser.UserInterface.Windows10
});
}
- private void Taskbar_Closing(object sender, System.ComponentModel.CancelEventArgs e)
+ private void Taskbar_QuitButtonClicked()
+ {
+ QuitButtonClicked?.Invoke();
+ }
+
+ private void Taskbar_Closing(object sender, CancelEventArgs e)
{
foreach (var child in SystemControlStackPanel.Children)
{
diff --git a/SafeExamBrowser.WindowsApi/Process.cs b/SafeExamBrowser.WindowsApi/Process.cs
index 1b6cd321..967e39eb 100644
--- a/SafeExamBrowser.WindowsApi/Process.cs
+++ b/SafeExamBrowser.WindowsApi/Process.cs
@@ -14,14 +14,33 @@ namespace SafeExamBrowser.WindowsApi
{
private System.Diagnostics.Process process;
+ public event ProcessTerminatedEventHandler Terminated;
+
public int Id
{
get { return process.Id; }
}
+ public bool HasTerminated
+ {
+ get { return process.HasExited; }
+ }
+
public Process(int id)
{
process = System.Diagnostics.Process.GetProcessById(id);
+ process.Exited += Process_Exited;
+ process.EnableRaisingEvents = true;
+ }
+
+ public void Kill()
+ {
+ process.Kill();
+ }
+
+ private void Process_Exited(object sender, System.EventArgs e)
+ {
+ Terminated?.Invoke(process.ExitCode);
}
}
}