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); } } }