SEBWIN-219: Basic startup sequence is now working.
This commit is contained in:
parent
d935407ecb
commit
d3dea29ecd
23 changed files with 329 additions and 114 deletions
|
@ -7,7 +7,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
|
||||||
|
@ -56,14 +55,16 @@ namespace SafeExamBrowser.Client
|
||||||
{
|
{
|
||||||
base.OnStartup(e);
|
base.OnStartup(e);
|
||||||
|
|
||||||
|
ShutdownMode = ShutdownMode.OnMainWindowClose;
|
||||||
|
|
||||||
instances.BuildObjectGraph();
|
instances.BuildObjectGraph();
|
||||||
|
instances.LogStartupInformation();
|
||||||
|
|
||||||
var success = instances.ClientController.TryStart();
|
var success = instances.ClientController.TryStart();
|
||||||
|
|
||||||
if (success)
|
if (success)
|
||||||
{
|
{
|
||||||
MainWindow = instances.Taskbar;
|
MainWindow = instances.Taskbar;
|
||||||
MainWindow.Closing += MainWindow_Closing;
|
|
||||||
MainWindow.Show();
|
MainWindow.Show();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -72,10 +73,12 @@ namespace SafeExamBrowser.Client
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MainWindow_Closing(object sender, CancelEventArgs e)
|
protected override void OnExit(ExitEventArgs e)
|
||||||
{
|
{
|
||||||
MainWindow?.Hide();
|
|
||||||
instances.ClientController.Terminate();
|
instances.ClientController.Terminate();
|
||||||
|
instances.LogShutdownInformation();
|
||||||
|
|
||||||
|
base.OnExit(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,10 +17,13 @@ namespace SafeExamBrowser.Client.Communication
|
||||||
{
|
{
|
||||||
internal class ClientHost : BaseHost, IClientHost
|
internal class ClientHost : BaseHost, IClientHost
|
||||||
{
|
{
|
||||||
|
private int processId;
|
||||||
|
|
||||||
public Guid StartupToken { private get; set; }
|
public Guid StartupToken { private get; set; }
|
||||||
|
|
||||||
public ClientHost(string address, ILogger logger) : base(address, logger)
|
public ClientHost(string address, ILogger logger, int processId) : base(address, logger)
|
||||||
{
|
{
|
||||||
|
this.processId = processId;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnConnect(Guid? token)
|
protected override bool OnConnect(Guid? token)
|
||||||
|
@ -36,13 +39,19 @@ namespace SafeExamBrowser.Client.Communication
|
||||||
protected override Response OnReceive(Message message)
|
protected override Response OnReceive(Message message)
|
||||||
{
|
{
|
||||||
// TODO
|
// TODO
|
||||||
return null;
|
|
||||||
|
return new SimpleResponse(SimpleResponsePurport.UnknownMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Response OnReceive(MessagePurport message)
|
protected override Response OnReceive(SimpleMessagePurport message)
|
||||||
{
|
{
|
||||||
// TODO
|
switch (message)
|
||||||
return null;
|
{
|
||||||
|
case SimpleMessagePurport.Authenticate:
|
||||||
|
return new AuthenticationResponse { ProcessId = processId };
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SimpleResponse(SimpleResponsePurport.UnknownMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using SafeExamBrowser.Browser;
|
using SafeExamBrowser.Browser;
|
||||||
using SafeExamBrowser.Client.Behaviour;
|
using SafeExamBrowser.Client.Behaviour;
|
||||||
using SafeExamBrowser.Client.Behaviour.Operations;
|
using SafeExamBrowser.Client.Behaviour.Operations;
|
||||||
|
@ -37,6 +38,10 @@ namespace SafeExamBrowser.Client
|
||||||
{
|
{
|
||||||
internal class CompositionRoot
|
internal class CompositionRoot
|
||||||
{
|
{
|
||||||
|
private string logFilePath;
|
||||||
|
private string runtimeHostUri;
|
||||||
|
private Guid startupToken;
|
||||||
|
|
||||||
private ClientConfiguration configuration;
|
private ClientConfiguration configuration;
|
||||||
private ILogger logger;
|
private ILogger logger;
|
||||||
private INativeMethods nativeMethods;
|
private INativeMethods nativeMethods;
|
||||||
|
@ -49,21 +54,19 @@ namespace SafeExamBrowser.Client
|
||||||
|
|
||||||
internal void BuildObjectGraph()
|
internal void BuildObjectGraph()
|
||||||
{
|
{
|
||||||
var args = Environment.GetCommandLineArgs();
|
ValidateCommandLineArguments();
|
||||||
|
|
||||||
Validate(args);
|
|
||||||
|
|
||||||
configuration = new ClientConfiguration();
|
configuration = new ClientConfiguration();
|
||||||
logger = new Logger();
|
logger = new Logger();
|
||||||
nativeMethods = new NativeMethods();
|
nativeMethods = new NativeMethods();
|
||||||
systemInfo = new SystemInfo();
|
systemInfo = new SystemInfo();
|
||||||
|
|
||||||
InitializeLogging(args[1]);
|
InitializeLogging();
|
||||||
|
|
||||||
text = new Text(logger);
|
text = new Text(logger);
|
||||||
uiFactory = new UserInterfaceFactory(text);
|
uiFactory = new UserInterfaceFactory(text);
|
||||||
|
|
||||||
var runtimeProxy = new RuntimeProxy(args[2], new ModuleLogger(logger, typeof(RuntimeProxy)));
|
var runtimeProxy = new RuntimeProxy(runtimeHostUri, new ModuleLogger(logger, typeof(RuntimeProxy)));
|
||||||
var displayMonitor = new DisplayMonitor(new ModuleLogger(logger, typeof(DisplayMonitor)), nativeMethods);
|
var displayMonitor = new DisplayMonitor(new ModuleLogger(logger, typeof(DisplayMonitor)), nativeMethods);
|
||||||
var processMonitor = new ProcessMonitor(new ModuleLogger(logger, typeof(ProcessMonitor)), nativeMethods);
|
var processMonitor = new ProcessMonitor(new ModuleLogger(logger, typeof(ProcessMonitor)), nativeMethods);
|
||||||
var windowMonitor = new WindowMonitor(new ModuleLogger(logger, typeof(WindowMonitor)), nativeMethods);
|
var windowMonitor = new WindowMonitor(new ModuleLogger(logger, typeof(WindowMonitor)), nativeMethods);
|
||||||
|
@ -73,25 +76,38 @@ namespace SafeExamBrowser.Client
|
||||||
var operations = new Queue<IOperation>();
|
var operations = new Queue<IOperation>();
|
||||||
|
|
||||||
operations.Enqueue(new I18nOperation(logger, text));
|
operations.Enqueue(new I18nOperation(logger, text));
|
||||||
operations.Enqueue(new RuntimeConnectionOperation(logger, runtimeProxy, Guid.Parse(args[3])));
|
operations.Enqueue(new RuntimeConnectionOperation(logger, runtimeProxy, startupToken));
|
||||||
operations.Enqueue(new ConfigurationOperation(configuration, logger, runtimeProxy));
|
operations.Enqueue(new ConfigurationOperation(configuration, logger, runtimeProxy));
|
||||||
operations.Enqueue(new DelayedInitializationOperation(BuildCommunicationHostOperation));
|
operations.Enqueue(new DelayedInitializationOperation(BuildCommunicationHostOperation));
|
||||||
operations.Enqueue(new DelayedInitializationOperation(BuildKeyboardInterceptorOperation));
|
// TODO
|
||||||
operations.Enqueue(new WindowMonitorOperation(logger, windowMonitor));
|
//operations.Enqueue(new DelayedInitializationOperation(BuildKeyboardInterceptorOperation));
|
||||||
operations.Enqueue(new ProcessMonitorOperation(logger, processMonitor));
|
//operations.Enqueue(new WindowMonitorOperation(logger, windowMonitor));
|
||||||
|
//operations.Enqueue(new ProcessMonitorOperation(logger, processMonitor));
|
||||||
operations.Enqueue(new DisplayMonitorOperation(displayMonitor, logger, Taskbar));
|
operations.Enqueue(new DisplayMonitorOperation(displayMonitor, logger, Taskbar));
|
||||||
operations.Enqueue(new DelayedInitializationOperation(BuildTaskbarOperation));
|
operations.Enqueue(new DelayedInitializationOperation(BuildTaskbarOperation));
|
||||||
operations.Enqueue(new DelayedInitializationOperation(BuildBrowserOperation));
|
operations.Enqueue(new DelayedInitializationOperation(BuildBrowserOperation));
|
||||||
operations.Enqueue(new ClipboardOperation(logger, nativeMethods));
|
operations.Enqueue(new ClipboardOperation(logger, nativeMethods));
|
||||||
operations.Enqueue(new DelayedInitializationOperation(BuildMouseInterceptorOperation));
|
//operations.Enqueue(new DelayedInitializationOperation(BuildMouseInterceptorOperation));
|
||||||
|
|
||||||
var sequence = new OperationSequence(logger, operations);
|
var sequence = new OperationSequence(logger, operations);
|
||||||
|
|
||||||
ClientController = new ClientController(displayMonitor, logger, sequence, processMonitor, runtimeProxy, Taskbar, windowMonitor);
|
ClientController = new ClientController(displayMonitor, logger, sequence, processMonitor, runtimeProxy, Taskbar, windowMonitor);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Validate(string[] args)
|
internal void LogStartupInformation()
|
||||||
{
|
{
|
||||||
|
logger.Log($"# New client instance started at {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
|
||||||
|
logger.Log(string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void LogShutdownInformation()
|
||||||
|
{
|
||||||
|
logger?.Log($"# Client instance terminated at {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ValidateCommandLineArguments()
|
||||||
|
{
|
||||||
|
var args = Environment.GetCommandLineArgs();
|
||||||
var hasFour = args?.Length == 4;
|
var hasFour = args?.Length == 4;
|
||||||
|
|
||||||
if (hasFour)
|
if (hasFour)
|
||||||
|
@ -102,16 +118,20 @@ namespace SafeExamBrowser.Client
|
||||||
|
|
||||||
if (hasLogfilePath && hasHostUri && hasToken)
|
if (hasLogfilePath && hasHostUri && hasToken)
|
||||||
{
|
{
|
||||||
|
logFilePath = args[1];
|
||||||
|
runtimeHostUri = args[2];
|
||||||
|
startupToken = Guid.Parse(args[3]);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new ArgumentException("Invalid parameters! Required: SafeExamBrowser.Client.exe <logfile path> <host URI> <token>");
|
throw new ArgumentException("Invalid arguments! Required: SafeExamBrowser.Client.exe <logfile path> <host URI> <token>");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeLogging(string filePath)
|
private void InitializeLogging()
|
||||||
{
|
{
|
||||||
var logFileWriter = new LogFileWriter(new DefaultLogFormatter(), filePath);
|
var logFileWriter = new LogFileWriter(new DefaultLogFormatter(), logFilePath);
|
||||||
|
|
||||||
logFileWriter.Initialize();
|
logFileWriter.Initialize();
|
||||||
logger.Subscribe(logFileWriter);
|
logger.Subscribe(logFileWriter);
|
||||||
|
@ -128,9 +148,12 @@ namespace SafeExamBrowser.Client
|
||||||
|
|
||||||
private IOperation BuildCommunicationHostOperation()
|
private IOperation BuildCommunicationHostOperation()
|
||||||
{
|
{
|
||||||
var host = new ClientHost(configuration.RuntimeInfo.ClientAddress, new ModuleLogger(logger, typeof(ClientHost)));
|
var processId = Process.GetCurrentProcess().Id;
|
||||||
|
var host = new ClientHost(configuration.RuntimeInfo.ClientAddress, new ModuleLogger(logger, typeof(ClientHost)), processId);
|
||||||
var operation = new CommunicationOperation(host, logger);
|
var operation = new CommunicationOperation(host, logger);
|
||||||
|
|
||||||
|
host.StartupToken = startupToken;
|
||||||
|
|
||||||
return operation;
|
return operation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -73,6 +73,9 @@ namespace SafeExamBrowser.Configuration
|
||||||
};
|
};
|
||||||
|
|
||||||
settings.Browser.StartUrl = "https://www.duckduckgo.com";
|
settings.Browser.StartUrl = "https://www.duckduckgo.com";
|
||||||
|
settings.Taskbar.AllowApplicationLog = true;
|
||||||
|
settings.Taskbar.AllowKeyboardLayout = true;
|
||||||
|
settings.Taskbar.AllowWirelessNetwork = true;
|
||||||
|
|
||||||
CurrentSettings = settings;
|
CurrentSettings = settings;
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ namespace SafeExamBrowser.Contracts.Communication
|
||||||
[ServiceKnownType(typeof(AuthenticationResponse))]
|
[ServiceKnownType(typeof(AuthenticationResponse))]
|
||||||
[ServiceKnownType(typeof(ConfigurationResponse))]
|
[ServiceKnownType(typeof(ConfigurationResponse))]
|
||||||
[ServiceKnownType(typeof(ClientConfiguration))]
|
[ServiceKnownType(typeof(ClientConfiguration))]
|
||||||
|
[ServiceKnownType(typeof(SimpleResponse))]
|
||||||
public interface ICommunication
|
public interface ICommunication
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -20,11 +20,13 @@ namespace SafeExamBrowser.Contracts.Communication
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Starts the host and opens it for communication.
|
/// Starts the host and opens it for communication.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <exception cref="System.ServiceModel.CommunicationException">If the host fails to start.</exception>
|
||||||
void Start();
|
void Start();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Closes and terminates the host.
|
/// Closes and terminates the host.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <exception cref="System.ServiceModel.CommunicationException">If the host fails to terminate.</exception>
|
||||||
void Stop();
|
void Stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,5 +17,10 @@ namespace SafeExamBrowser.Contracts.Communication.Messages
|
||||||
/// The communication token needed for authentication.
|
/// The communication token needed for authentication.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Guid CommunicationToken { get; set; }
|
public Guid CommunicationToken { get; set; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return GetType().Name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,16 @@ namespace SafeExamBrowser.Contracts.Communication.Messages
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The purport of the message.
|
/// The purport of the message.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public MessagePurport Purport { get; set; }
|
public SimpleMessagePurport Purport { get; set; }
|
||||||
|
|
||||||
|
public SimpleMessage(SimpleMessagePurport purport)
|
||||||
|
{
|
||||||
|
Purport = purport;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{base.ToString()} -> {Purport}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ using System;
|
||||||
namespace SafeExamBrowser.Contracts.Communication.Messages
|
namespace SafeExamBrowser.Contracts.Communication.Messages
|
||||||
{
|
{
|
||||||
[Serializable]
|
[Serializable]
|
||||||
public enum MessagePurport
|
public enum SimpleMessagePurport
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Requests an interlocutor to submit data for authentication.
|
/// Requests an interlocutor to submit data for authentication.
|
|
@ -16,6 +16,6 @@ namespace SafeExamBrowser.Contracts.Communication.Responses
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The process identifier used for authentication.
|
/// The process identifier used for authentication.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int ProcessId { get; }
|
public int ProcessId { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,5 +13,9 @@ namespace SafeExamBrowser.Contracts.Communication.Responses
|
||||||
[Serializable]
|
[Serializable]
|
||||||
public class Response
|
public class Response
|
||||||
{
|
{
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return GetType().Name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2018 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.Contracts.Communication.Responses
|
||||||
|
{
|
||||||
|
[Serializable]
|
||||||
|
public class SimpleResponse : Response
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The purport of the response.
|
||||||
|
/// </summary>
|
||||||
|
public SimpleResponsePurport Purport { get; set; }
|
||||||
|
|
||||||
|
public SimpleResponse(SimpleResponsePurport purport)
|
||||||
|
{
|
||||||
|
Purport = purport;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{base.ToString()} -> {Purport}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2018 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.Contracts.Communication.Responses
|
||||||
|
{
|
||||||
|
[Serializable]
|
||||||
|
public enum SimpleResponsePurport
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Signals an interlocutor that a message has been understood.
|
||||||
|
/// </summary>
|
||||||
|
Acknowledged = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Signals an interlocutor that a message has not been understood.
|
||||||
|
/// </summary>
|
||||||
|
UnknownMessage
|
||||||
|
}
|
||||||
|
}
|
|
@ -66,13 +66,15 @@
|
||||||
<Compile Include="Communication\IServiceProxy.cs" />
|
<Compile Include="Communication\IServiceProxy.cs" />
|
||||||
<Compile Include="Communication\Messages\Message.cs" />
|
<Compile Include="Communication\Messages\Message.cs" />
|
||||||
<Compile Include="Communication\Messages\DisconnectionMessage.cs" />
|
<Compile Include="Communication\Messages\DisconnectionMessage.cs" />
|
||||||
<Compile Include="Communication\Messages\MessagePurport.cs" />
|
<Compile Include="Communication\Messages\SimpleMessagePurport.cs" />
|
||||||
<Compile Include="Communication\Messages\SimpleMessage.cs" />
|
<Compile Include="Communication\Messages\SimpleMessage.cs" />
|
||||||
<Compile Include="Communication\Responses\AuthenticationResponse.cs" />
|
<Compile Include="Communication\Responses\AuthenticationResponse.cs" />
|
||||||
<Compile Include="Communication\Responses\ConfigurationResponse.cs" />
|
<Compile Include="Communication\Responses\ConfigurationResponse.cs" />
|
||||||
<Compile Include="Communication\Responses\ConnectionResponse.cs" />
|
<Compile Include="Communication\Responses\ConnectionResponse.cs" />
|
||||||
<Compile Include="Communication\Responses\DisconnectionResponse.cs" />
|
<Compile Include="Communication\Responses\DisconnectionResponse.cs" />
|
||||||
<Compile Include="Communication\Responses\Response.cs" />
|
<Compile Include="Communication\Responses\Response.cs" />
|
||||||
|
<Compile Include="Communication\Responses\SimpleResponsePurport.cs" />
|
||||||
|
<Compile Include="Communication\Responses\SimpleResponse.cs" />
|
||||||
<Compile Include="Configuration\ClientConfiguration.cs" />
|
<Compile Include="Configuration\ClientConfiguration.cs" />
|
||||||
<Compile Include="Configuration\RuntimeInfo.cs" />
|
<Compile Include="Configuration\RuntimeInfo.cs" />
|
||||||
<Compile Include="Configuration\ISession.cs" />
|
<Compile Include="Configuration\ISession.cs" />
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.ServiceModel;
|
using System.ServiceModel;
|
||||||
|
using System.Threading;
|
||||||
using SafeExamBrowser.Contracts.Communication;
|
using SafeExamBrowser.Contracts.Communication;
|
||||||
using SafeExamBrowser.Contracts.Communication.Messages;
|
using SafeExamBrowser.Contracts.Communication.Messages;
|
||||||
using SafeExamBrowser.Contracts.Communication.Responses;
|
using SafeExamBrowser.Contracts.Communication.Responses;
|
||||||
|
@ -18,16 +19,26 @@ namespace SafeExamBrowser.Core.Communication
|
||||||
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Single)]
|
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Single)]
|
||||||
public abstract class BaseHost : ICommunication, ICommunicationHost
|
public abstract class BaseHost : ICommunication, ICommunicationHost
|
||||||
{
|
{
|
||||||
|
private const int TWO_SECONDS = 2000;
|
||||||
|
private readonly object @lock = new object();
|
||||||
|
|
||||||
private string address;
|
private string address;
|
||||||
private ILogger logger;
|
private ILogger logger;
|
||||||
private ServiceHost host;
|
private ServiceHost host;
|
||||||
|
private Thread hostThread;
|
||||||
|
|
||||||
protected Guid? CommunicationToken { get; private set; }
|
protected Guid? CommunicationToken { get; private set; }
|
||||||
protected ILogger Logger { get; private set; }
|
protected ILogger Logger { get; private set; }
|
||||||
|
|
||||||
public bool IsRunning
|
public bool IsRunning
|
||||||
{
|
{
|
||||||
get { return host?.State == CommunicationState.Opened; }
|
get
|
||||||
|
{
|
||||||
|
lock (@lock)
|
||||||
|
{
|
||||||
|
return host?.State == CommunicationState.Opened;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public BaseHost(string address, ILogger logger)
|
public BaseHost(string address, ILogger logger)
|
||||||
|
@ -39,86 +50,111 @@ namespace SafeExamBrowser.Core.Communication
|
||||||
protected abstract bool OnConnect(Guid? token);
|
protected abstract bool OnConnect(Guid? token);
|
||||||
protected abstract void OnDisconnect();
|
protected abstract void OnDisconnect();
|
||||||
protected abstract Response OnReceive(Message message);
|
protected abstract Response OnReceive(Message message);
|
||||||
protected abstract Response OnReceive(MessagePurport message);
|
protected abstract Response OnReceive(SimpleMessagePurport message);
|
||||||
|
|
||||||
public ConnectionResponse Connect(Guid? token = null)
|
public ConnectionResponse Connect(Guid? token = null)
|
||||||
{
|
{
|
||||||
logger.Debug($"Received connection request with authentication token '{token}'.");
|
lock (@lock)
|
||||||
|
|
||||||
var response = new ConnectionResponse();
|
|
||||||
var connected = OnConnect(token);
|
|
||||||
|
|
||||||
if (connected)
|
|
||||||
{
|
{
|
||||||
response.CommunicationToken = CommunicationToken = Guid.NewGuid();
|
logger.Debug($"Received connection request with authentication token '{token}'.");
|
||||||
response.ConnectionEstablished = true;
|
|
||||||
|
var response = new ConnectionResponse();
|
||||||
|
var connected = OnConnect(token);
|
||||||
|
|
||||||
|
if (connected)
|
||||||
|
{
|
||||||
|
response.CommunicationToken = CommunicationToken = Guid.NewGuid();
|
||||||
|
response.ConnectionEstablished = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug($"{(connected ? "Accepted" : "Denied")} connection request.");
|
||||||
|
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Debug($"{(connected ? "Accepted" : "Denied")} connection request.");
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public DisconnectionResponse Disconnect(DisconnectionMessage message)
|
public DisconnectionResponse Disconnect(DisconnectionMessage message)
|
||||||
{
|
{
|
||||||
var response = new DisconnectionResponse();
|
lock (@lock)
|
||||||
|
|
||||||
// TODO: Compare with ToString in BaseProxy - needed?
|
|
||||||
logger.Debug($"Received disconnection request with message '{message}'.");
|
|
||||||
|
|
||||||
if (IsAuthorized(message?.CommunicationToken))
|
|
||||||
{
|
{
|
||||||
OnDisconnect();
|
var response = new DisconnectionResponse();
|
||||||
|
|
||||||
CommunicationToken = null;
|
logger.Debug($"Received disconnection request with message '{ToString(message)}'.");
|
||||||
response.ConnectionTerminated = true;
|
|
||||||
|
if (IsAuthorized(message?.CommunicationToken))
|
||||||
|
{
|
||||||
|
OnDisconnect();
|
||||||
|
|
||||||
|
CommunicationToken = null;
|
||||||
|
response.ConnectionTerminated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Response Send(Message message)
|
public Response Send(Message message)
|
||||||
{
|
{
|
||||||
Response response = null;
|
lock (@lock)
|
||||||
|
|
||||||
logger.Debug($"Received message '{message}'.");
|
|
||||||
|
|
||||||
if (IsAuthorized(message?.CommunicationToken))
|
|
||||||
{
|
{
|
||||||
if (message is SimpleMessage)
|
Response response = null;
|
||||||
|
|
||||||
|
logger.Debug($"Received message '{ToString(message)}'.");
|
||||||
|
|
||||||
|
if (IsAuthorized(message?.CommunicationToken))
|
||||||
{
|
{
|
||||||
response = OnReceive((message as SimpleMessage).Purport);
|
if (message is SimpleMessage)
|
||||||
}
|
{
|
||||||
else
|
response = OnReceive((message as SimpleMessage).Purport);
|
||||||
{
|
}
|
||||||
response = OnReceive(message);
|
else
|
||||||
|
{
|
||||||
|
response = OnReceive(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.Debug($"Sending response '{ToString(response)}'.");
|
||||||
|
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Debug($"Sending response '{response}'.");
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Start()
|
public void Start()
|
||||||
{
|
{
|
||||||
host = new ServiceHost(this);
|
lock (@lock)
|
||||||
host.AddServiceEndpoint(typeof(ICommunication), new NetNamedPipeBinding(NetNamedPipeSecurityMode.Transport), address);
|
{
|
||||||
host.Closed += Host_Closed;
|
var exception = default(Exception);
|
||||||
host.Closing += Host_Closing;
|
var startedEvent = new AutoResetEvent(false);
|
||||||
host.Faulted += Host_Faulted;
|
|
||||||
host.Opened += Host_Opened;
|
|
||||||
host.Opening += Host_Opening;
|
|
||||||
host.UnknownMessageReceived += Host_UnknownMessageReceived;
|
|
||||||
host.Open();
|
|
||||||
|
|
||||||
logger.Debug($"Successfully started communication host for endpoint '{address}'.");
|
hostThread = new Thread(() => TryStartHost(startedEvent, out exception));
|
||||||
|
hostThread.SetApartmentState(ApartmentState.STA);
|
||||||
|
hostThread.IsBackground = true;
|
||||||
|
hostThread.Start();
|
||||||
|
|
||||||
|
var success = startedEvent.WaitOne(TWO_SECONDS);
|
||||||
|
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
throw new CommunicationException($"Failed to start communication host for endpoint '{address}' within {TWO_SECONDS / 1000} seconds!", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Stop()
|
public void Stop()
|
||||||
{
|
{
|
||||||
host?.Close();
|
lock (@lock)
|
||||||
logger.Debug($"Terminated communication host for endpoint '{address}'.");
|
{
|
||||||
|
var success = TryStopHost(out Exception exception);
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
logger.Debug($"Terminated communication host for endpoint '{address}'.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new CommunicationException($"Failed to terminate communication host for endpoint '{address}'!", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsAuthorized(Guid? token)
|
private bool IsAuthorized(Guid? token)
|
||||||
|
@ -126,6 +162,52 @@ namespace SafeExamBrowser.Core.Communication
|
||||||
return CommunicationToken == token;
|
return CommunicationToken == token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void TryStartHost(AutoResetEvent startedEvent, out Exception exception)
|
||||||
|
{
|
||||||
|
exception = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
host = new ServiceHost(this);
|
||||||
|
host.AddServiceEndpoint(typeof(ICommunication), new NetNamedPipeBinding(NetNamedPipeSecurityMode.Transport), address);
|
||||||
|
host.Closed += Host_Closed;
|
||||||
|
host.Closing += Host_Closing;
|
||||||
|
host.Faulted += Host_Faulted;
|
||||||
|
host.Opened += Host_Opened;
|
||||||
|
host.Opening += Host_Opening;
|
||||||
|
host.UnknownMessageReceived += Host_UnknownMessageReceived;
|
||||||
|
host.Open();
|
||||||
|
|
||||||
|
logger.Debug($"Successfully started communication host for endpoint '{address}'.");
|
||||||
|
|
||||||
|
startedEvent.Set();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
exception = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryStopHost(out Exception exception)
|
||||||
|
{
|
||||||
|
var success = false;
|
||||||
|
|
||||||
|
exception = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
host?.Close();
|
||||||
|
success = hostThread.Join(TWO_SECONDS);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
exception = e;
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
private void Host_Closed(object sender, EventArgs e)
|
private void Host_Closed(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
logger.Debug("Communication host has been closed.");
|
logger.Debug("Communication host has been closed.");
|
||||||
|
@ -155,5 +237,15 @@ namespace SafeExamBrowser.Core.Communication
|
||||||
{
|
{
|
||||||
logger.Debug($"Communication host has received an unknown message: {e?.Message}.");
|
logger.Debug($"Communication host has received an unknown message: {e?.Message}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string ToString(Message message)
|
||||||
|
{
|
||||||
|
return message != null ? message.ToString() : "<null>";
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ToString(Response response)
|
||||||
|
{
|
||||||
|
return response != null ? response.ToString() : "<null>";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ namespace SafeExamBrowser.Core.Communication
|
||||||
(channel as ICommunicationObject).Opened += BaseProxy_Opened;
|
(channel as ICommunicationObject).Opened += BaseProxy_Opened;
|
||||||
(channel as ICommunicationObject).Opening += BaseProxy_Opening;
|
(channel as ICommunicationObject).Opening += BaseProxy_Opening;
|
||||||
|
|
||||||
Logger.Debug($"Trying to connect to endpoint {address} with authentication token '{token}'...");
|
Logger.Debug($"Trying to connect to endpoint {address}{(token.HasValue ? $" with authentication token '{token}'" : string.Empty)}...");
|
||||||
|
|
||||||
var response = channel.Connect(token);
|
var response = channel.Connect(token);
|
||||||
|
|
||||||
|
@ -70,21 +70,14 @@ namespace SafeExamBrowser.Core.Communication
|
||||||
|
|
||||||
var response = channel.Send(message);
|
var response = channel.Send(message);
|
||||||
|
|
||||||
Logger.Debug($"Sent {ToString(message)}, got {ToString(response)}.");
|
Logger.Debug($"Sent message '{ToString(message)}', got response '{ToString(response)}'.");
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Response Send(MessagePurport purport)
|
protected Response Send(SimpleMessagePurport purport)
|
||||||
{
|
{
|
||||||
FailIfNotConnected(nameof(Send));
|
return Send(new SimpleMessage(purport));
|
||||||
|
|
||||||
var message = new SimpleMessage { Purport = purport };
|
|
||||||
var response = channel.Send(message);
|
|
||||||
|
|
||||||
Logger.Debug($"Sent {ToString(message)}, got {ToString(response)}.");
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BaseProxy_Closed(object sender, EventArgs e)
|
private void BaseProxy_Closed(object sender, EventArgs e)
|
||||||
|
@ -132,12 +125,12 @@ namespace SafeExamBrowser.Core.Communication
|
||||||
|
|
||||||
private string ToString(Message message)
|
private string ToString(Message message)
|
||||||
{
|
{
|
||||||
return message != null ? $"message of type '{message.GetType()}'" : "no message";
|
return message != null ? message.ToString() : "<null>";
|
||||||
}
|
}
|
||||||
|
|
||||||
private string ToString(Response response)
|
private string ToString(Response response)
|
||||||
{
|
{
|
||||||
return response != null ? $"response of type '{response.GetType()}'" : "no response";
|
return response != null ? response.ToString() : "<null>";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,9 @@ namespace SafeExamBrowser.Core.Communication
|
||||||
|
|
||||||
public AuthenticationResponse RequestAuthentication()
|
public AuthenticationResponse RequestAuthentication()
|
||||||
{
|
{
|
||||||
return (AuthenticationResponse) Send(MessagePurport.ClientIsReady);
|
var response = Send(SimpleMessagePurport.Authenticate);
|
||||||
|
|
||||||
|
return response as AuthenticationResponse;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,12 +22,12 @@ namespace SafeExamBrowser.Core.Communication
|
||||||
|
|
||||||
public ClientConfiguration GetConfiguration()
|
public ClientConfiguration GetConfiguration()
|
||||||
{
|
{
|
||||||
return ((ConfigurationResponse) Send(MessagePurport.ConfigurationNeeded)).Configuration;
|
return ((ConfigurationResponse) Send(SimpleMessagePurport.ConfigurationNeeded)).Configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void InformClientReady()
|
public void InformClientReady()
|
||||||
{
|
{
|
||||||
Send(MessagePurport.ClientIsReady);
|
Send(SimpleMessagePurport.ClientIsReady);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,6 +81,15 @@ namespace SafeExamBrowser.Core.Logging
|
||||||
details.AppendLine();
|
details.AppendLine();
|
||||||
details.AppendLine(exception.StackTrace);
|
details.AppendLine(exception.StackTrace);
|
||||||
|
|
||||||
|
for (var inner = exception.InnerException; inner != null; inner = inner.InnerException)
|
||||||
|
{
|
||||||
|
details.AppendLine();
|
||||||
|
details.AppendLine($" Inner Exception Message: {inner.Message}");
|
||||||
|
details.AppendLine($" Inner Exception Type: {inner.GetType()}");
|
||||||
|
details.AppendLine();
|
||||||
|
details.AppendLine(inner.StackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
Add(LogLevel.Error, message);
|
Add(LogLevel.Error, message);
|
||||||
Add(new LogText(details.ToString()));
|
Add(new LogText(details.ToString()));
|
||||||
}
|
}
|
||||||
|
|
|
@ -104,19 +104,19 @@ namespace SafeExamBrowser.Runtime.Behaviour.Operations
|
||||||
{
|
{
|
||||||
const int TEN_SECONDS = 10000;
|
const int TEN_SECONDS = 10000;
|
||||||
|
|
||||||
var clientReady = new AutoResetEvent(false);
|
var clientStarted = false;
|
||||||
var clientReadyHandler = new CommunicationEventHandler(() => clientReady.Set());
|
var clientReadyEvent = new AutoResetEvent(false);
|
||||||
|
var clientReadyEventHandler = new CommunicationEventHandler(() => clientReadyEvent.Set());
|
||||||
var clientExecutable = configuration.RuntimeInfo.ClientExecutablePath;
|
var clientExecutable = configuration.RuntimeInfo.ClientExecutablePath;
|
||||||
var clientLogFile = $"{'"' + configuration.RuntimeInfo.ClientLogFile + '"'}";
|
var clientLogFile = $"{'"' + configuration.RuntimeInfo.ClientLogFile + '"'}";
|
||||||
var hostUri = configuration.RuntimeInfo.RuntimeAddress;
|
var hostUri = configuration.RuntimeInfo.RuntimeAddress;
|
||||||
var token = session.StartupToken.ToString("D");
|
var token = session.StartupToken.ToString("D");
|
||||||
|
|
||||||
runtimeHost.ClientReady += clientReadyHandler;
|
runtimeHost.ClientReady += clientReadyEventHandler;
|
||||||
session.ClientProcess = processFactory.StartNew(clientExecutable, clientLogFile, hostUri, token);
|
session.ClientProcess = processFactory.StartNew(clientExecutable, clientLogFile, hostUri, token);
|
||||||
|
|
||||||
var clientStarted = clientReady.WaitOne(TEN_SECONDS);
|
clientStarted = clientReadyEvent.WaitOne(TEN_SECONDS);
|
||||||
|
runtimeHost.ClientReady -= clientReadyEventHandler;
|
||||||
runtimeHost.ClientReady -= clientReadyHandler;
|
|
||||||
|
|
||||||
// TODO: Check if client process alive!
|
// TODO: Check if client process alive!
|
||||||
if (clientStarted)
|
if (clientStarted)
|
||||||
|
@ -126,7 +126,7 @@ namespace SafeExamBrowser.Runtime.Behaviour.Operations
|
||||||
var response = client.RequestAuthentication();
|
var response = client.RequestAuthentication();
|
||||||
|
|
||||||
// TODO: Further integrity checks necessary?
|
// TODO: Further integrity checks necessary?
|
||||||
if (session.ClientProcess.Id == response.ProcessId)
|
if (session.ClientProcess.Id == response?.ProcessId)
|
||||||
{
|
{
|
||||||
sessionRunning = true;
|
sessionRunning = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using SafeExamBrowser.Contracts.Communication;
|
using SafeExamBrowser.Contracts.Communication;
|
||||||
using SafeExamBrowser.Contracts.Communication.Messages;
|
using SafeExamBrowser.Contracts.Communication.Messages;
|
||||||
using SafeExamBrowser.Contracts.Communication.Responses;
|
using SafeExamBrowser.Contracts.Communication.Responses;
|
||||||
|
@ -43,21 +42,21 @@ namespace SafeExamBrowser.Runtime.Communication
|
||||||
protected override Response OnReceive(Message message)
|
protected override Response OnReceive(Message message)
|
||||||
{
|
{
|
||||||
// TODO
|
// TODO
|
||||||
return null;
|
return new SimpleResponse(SimpleResponsePurport.UnknownMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Response OnReceive(MessagePurport message)
|
protected override Response OnReceive(SimpleMessagePurport message)
|
||||||
{
|
{
|
||||||
switch (message)
|
switch (message)
|
||||||
{
|
{
|
||||||
case MessagePurport.ClientIsReady:
|
case SimpleMessagePurport.ClientIsReady:
|
||||||
ClientReady?.Invoke();
|
ClientReady?.Invoke();
|
||||||
break;
|
return new SimpleResponse(SimpleResponsePurport.Acknowledged);
|
||||||
case MessagePurport.ConfigurationNeeded:
|
case SimpleMessagePurport.ConfigurationNeeded:
|
||||||
return new ConfigurationResponse { Configuration = configuration.BuildClientConfiguration() };
|
return new ConfigurationResponse { Configuration = configuration.BuildClientConfiguration() };
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return new SimpleResponse(SimpleResponsePurport.UnknownMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ namespace SafeExamBrowser.SystemComponents
|
||||||
public class WirelessNetwork : ISystemComponent<ISystemWirelessNetworkControl>
|
public class WirelessNetwork : ISystemComponent<ISystemWirelessNetworkControl>
|
||||||
{
|
{
|
||||||
private const int TWO_SECONDS = 2000;
|
private const int TWO_SECONDS = 2000;
|
||||||
|
private readonly object @lock = new object();
|
||||||
|
|
||||||
private ISystemWirelessNetworkControl control;
|
private ISystemWirelessNetworkControl control;
|
||||||
private ILogger logger;
|
private ILogger logger;
|
||||||
|
@ -144,7 +145,7 @@ namespace SafeExamBrowser.SystemComponents
|
||||||
|
|
||||||
private void UpdateControl()
|
private void UpdateControl()
|
||||||
{
|
{
|
||||||
lock (networks)
|
lock (@lock)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Classic"
|
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Classic"
|
||||||
xmlns:s="clr-namespace:System;assembly=mscorlib"
|
xmlns:s="clr-namespace:System;assembly=mscorlib"
|
||||||
mc:Ignorable="d" Background="White" Foreground="White" Height="500" Width="750" WindowStyle="None" WindowStartupLocation="CenterScreen"
|
mc:Ignorable="d" Background="White" Foreground="White" Height="500" Width="850" WindowStyle="None" WindowStartupLocation="CenterScreen"
|
||||||
Icon="./Images/SafeExamBrowser.ico" ResizeMode="NoResize" Title="Safe Exam Browser" Topmost="True">
|
Icon="./Images/SafeExamBrowser.ico" ResizeMode="NoResize" Title="Safe Exam Browser" Topmost="True">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Border x:Name="AnimatedBorder" Panel.ZIndex="10" BorderBrush="DodgerBlue" BorderThickness="5" Visibility="{Binding AnimatedBorderVisibility}">
|
<Border x:Name="AnimatedBorder" Panel.ZIndex="10" BorderBrush="DodgerBlue" BorderThickness="5" Visibility="{Binding AnimatedBorderVisibility}">
|
||||||
|
@ -36,10 +36,10 @@
|
||||||
<Grid Grid.Row="0">
|
<Grid Grid.Row="0">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
<ColumnDefinition Width="350" />
|
<ColumnDefinition Width="400" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<Image Grid.Column="0" Grid.ColumnSpan="2" Margin="-25,0,0,0" Source="pack://application:,,,/SafeExamBrowser.UserInterface.Classic;component/Images/SplashScreen.png" />
|
<Image Grid.Column="0" Grid.ColumnSpan="2" Margin="-25,0,0,0" Source="pack://application:,,,/SafeExamBrowser.UserInterface.Classic;component/Images/SplashScreen.png" />
|
||||||
<TextBlock x:Name="InfoTextBlock" Grid.Column="1" Foreground="Gray" Margin="10,75,175,10" TextWrapping="Wrap" />
|
<TextBlock x:Name="InfoTextBlock" Grid.Column="1" Foreground="Gray" Margin="10,75,225,10" TextWrapping="Wrap" />
|
||||||
</Grid>
|
</Grid>
|
||||||
<ProgressBar x:Name="ProgressBar" Grid.Row="1" Background="WhiteSmoke" BorderThickness="0" Foreground="DodgerBlue"
|
<ProgressBar x:Name="ProgressBar" Grid.Row="1" Background="WhiteSmoke" BorderThickness="0" Foreground="DodgerBlue"
|
||||||
IsIndeterminate="{Binding IsIndeterminate}" Maximum="{Binding MaxProgress}" Value="{Binding CurrentProgress}"
|
IsIndeterminate="{Binding IsIndeterminate}" Maximum="{Binding MaxProgress}" Value="{Binding CurrentProgress}"
|
||||||
|
|
Loading…
Reference in a new issue