Completed basic implementation of window and process monitoring.
This commit is contained in:
parent
9ab04ecc77
commit
25105d61b1
19 changed files with 516 additions and 223 deletions
|
@ -53,6 +53,11 @@ namespace SafeExamBrowser.Browser
|
|||
|
||||
public void Terminate()
|
||||
{
|
||||
foreach (var instance in instances)
|
||||
{
|
||||
instance.Window.Close();
|
||||
}
|
||||
|
||||
Cef.Shutdown();
|
||||
}
|
||||
|
||||
|
|
|
@ -71,10 +71,6 @@
|
|||
<Project>{47DA5933-BEF8-4729-94E6-ABDE2DB12262}</Project>
|
||||
<Name>SafeExamBrowser.Contracts</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\SafeExamBrowser.WindowsApi\SafeExamBrowser.WindowsApi.csproj">
|
||||
<Project>{73724659-4150-4792-a94e-42f5f3c1b696}</Project>
|
||||
<Name>SafeExamBrowser.WindowsApi</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
|
@ -10,24 +10,26 @@ using System.Windows.Forms;
|
|||
using SafeExamBrowser.Contracts.Configuration;
|
||||
using SafeExamBrowser.Contracts.Logging;
|
||||
using SafeExamBrowser.Contracts.UserInterface;
|
||||
using SafeExamBrowser.WindowsApi;
|
||||
using SafeExamBrowser.WindowsApi.Types;
|
||||
using SafeExamBrowser.Contracts.WindowsApi;
|
||||
using SafeExamBrowser.Contracts.WindowsApi.Types;
|
||||
|
||||
namespace SafeExamBrowser.Configuration
|
||||
{
|
||||
public class WorkingArea : IWorkingArea
|
||||
{
|
||||
private ILogger logger;
|
||||
private INativeMethods nativeMethods;
|
||||
private RECT? originalWorkingArea;
|
||||
|
||||
public WorkingArea(ILogger logger)
|
||||
public WorkingArea(ILogger logger, INativeMethods nativeMethods)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.nativeMethods = nativeMethods;
|
||||
}
|
||||
|
||||
public void InitializeFor(ITaskbar taskbar)
|
||||
{
|
||||
originalWorkingArea = User32.GetWorkingArea();
|
||||
originalWorkingArea = nativeMethods.GetWorkingArea();
|
||||
|
||||
LogWorkingArea("Saved original working area", originalWorkingArea.Value);
|
||||
|
||||
|
@ -40,15 +42,15 @@ namespace SafeExamBrowser.Configuration
|
|||
};
|
||||
|
||||
LogWorkingArea("Trying to set new working area", area);
|
||||
User32.SetWorkingArea(area);
|
||||
LogWorkingArea("Working area is now set to", User32.GetWorkingArea());
|
||||
nativeMethods.SetWorkingArea(area);
|
||||
LogWorkingArea("Working area is now set to", nativeMethods.GetWorkingArea());
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
if (originalWorkingArea.HasValue)
|
||||
{
|
||||
User32.SetWorkingArea(originalWorkingArea.Value);
|
||||
nativeMethods.SetWorkingArea(originalWorkingArea.Value);
|
||||
LogWorkingArea("Restored original working area", originalWorkingArea.Value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
using System;
|
||||
|
||||
namespace SafeExamBrowser.Contracts.Monitoring
|
||||
{
|
||||
public delegate void ExplorerStartedHandler();
|
||||
|
@ -18,6 +20,17 @@ namespace SafeExamBrowser.Contracts.Monitoring
|
|||
/// </summary>
|
||||
event ExplorerStartedHandler ExplorerStarted;
|
||||
|
||||
/// <summary>
|
||||
/// Terminates the Windows explorer shell, i.e. the taskbar.
|
||||
/// </summary>
|
||||
void CloseExplorerShell();
|
||||
|
||||
/// <summary>
|
||||
/// Performs a check whether the process associated to the given window is allowed,
|
||||
/// i.e. whether the specified window should be hidden.
|
||||
/// </summary>
|
||||
void OnWindowChanged(IntPtr window, out bool hide);
|
||||
|
||||
/// <summary>
|
||||
/// Starts a new instance of the Windows explorer shell.
|
||||
/// </summary>
|
||||
|
@ -33,10 +46,5 @@ namespace SafeExamBrowser.Contracts.Monitoring
|
|||
/// Stops monitoring the Windows explorer.
|
||||
/// </summary>
|
||||
void StopMonitoringExplorer();
|
||||
|
||||
/// <summary>
|
||||
/// Terminates the Windows explorer shell, i.e. the taskbar.
|
||||
/// </summary>
|
||||
void CloseExplorerShell();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,10 +6,19 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
using System;
|
||||
|
||||
namespace SafeExamBrowser.Contracts.Monitoring
|
||||
{
|
||||
public delegate void WindowChangedHandler(IntPtr window, out bool hide);
|
||||
|
||||
public interface IWindowMonitor
|
||||
{
|
||||
/// <summary>
|
||||
/// Event fired when the window monitor observes that the foreground window has changed.
|
||||
/// </summary>
|
||||
event WindowChangedHandler WindowChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Hides all currently opened windows.
|
||||
/// </summary>
|
||||
|
|
|
@ -96,6 +96,8 @@
|
|||
<Compile Include="UserInterface\IWindow.cs" />
|
||||
<Compile Include="UserInterface\MessageBoxAction.cs" />
|
||||
<Compile Include="UserInterface\MessageBoxIcon.cs" />
|
||||
<Compile Include="WindowsApi\INativeMethods.cs" />
|
||||
<Compile Include="WindowsApi\Types\RECT.cs" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
|
@ -15,6 +15,11 @@ namespace SafeExamBrowser.Contracts.UserInterface
|
|||
/// </summary>
|
||||
void BringToForeground();
|
||||
|
||||
/// <summary>
|
||||
/// Closes the window.
|
||||
/// </summary>
|
||||
void Close();
|
||||
|
||||
/// <summary>
|
||||
/// Shows the window to the user.
|
||||
/// </summary>
|
||||
|
|
107
SafeExamBrowser.Contracts/WindowsApi/INativeMethods.cs
Normal file
107
SafeExamBrowser.Contracts/WindowsApi/INativeMethods.cs
Normal file
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* Copyright (c) 2017 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;
|
||||
using System.Collections.Generic;
|
||||
using SafeExamBrowser.Contracts.WindowsApi.Types;
|
||||
|
||||
namespace SafeExamBrowser.Contracts.WindowsApi
|
||||
{
|
||||
public interface INativeMethods
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves a collection of handles to all currently open (i.e. visible) windows.
|
||||
/// </summary>
|
||||
/// <exception cref="System.ComponentModel.Win32Exception">
|
||||
/// If the open windows could not be retrieved.
|
||||
/// </exception>
|
||||
IEnumerable<IntPtr> GetOpenWindows();
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the process identifier for the specified window handle.
|
||||
/// </summary>
|
||||
uint GetProcessIdFor(IntPtr window);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a window handle to the Windows taskbar. Returns <c>IntPtr.Zero</c>
|
||||
/// if the taskbar could not be found (i.e. if it isn't running).
|
||||
/// </summary>
|
||||
IntPtr GetShellWindowHandle();
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the process ID of the main Windows explorer instance controlling
|
||||
/// desktop and taskbar or <c>0</c>, if the process isn't running.
|
||||
/// </summary>
|
||||
uint GetShellProcessId();
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the title of the specified window, or an empty string, if the
|
||||
/// given window does not have a title.
|
||||
/// </summary>
|
||||
string GetWindowTitle(IntPtr window);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the currently configured working area of the primary screen.
|
||||
/// </summary>
|
||||
/// <exception cref="System.ComponentModel.Win32Exception">
|
||||
/// If the working area could not be retrieved.
|
||||
/// </exception>
|
||||
RECT GetWorkingArea();
|
||||
|
||||
/// <summary>
|
||||
/// Hides the given window.
|
||||
/// </summary>
|
||||
void HideWindow(IntPtr window);
|
||||
|
||||
/// <summary>
|
||||
/// Minimizes all open windows.
|
||||
/// </summary>
|
||||
void MinimizeAllOpenWindows();
|
||||
|
||||
/// <summary>
|
||||
/// Instructs the main Windows explorer process to shut down.
|
||||
/// </summary>
|
||||
/// <exception cref="System.ComponentModel.Win32Exception">
|
||||
/// If the message could not be successfully posted. Does not apply if the process isn't running!
|
||||
/// </exception>
|
||||
void PostCloseMessageToShell();
|
||||
|
||||
/// <summary>
|
||||
/// Registers a system event which will invoke the specified callback when the foreground window has changed.
|
||||
/// Returns a handle to the newly registered Windows event hook.
|
||||
/// </summary>
|
||||
IntPtr RegisterSystemForegroundEvent(Action<IntPtr> callback);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a system event which will invoke the specified callback when a window has received mouse capture.
|
||||
/// Returns a handle to the newly registered Windows event hook.
|
||||
/// </summary>
|
||||
IntPtr RegisterSystemCaptureStartEvent(Action<IntPtr> callback);
|
||||
|
||||
/// <summary>
|
||||
/// Restores the specified window to its original size and position.
|
||||
/// </summary>
|
||||
void RestoreWindow(IntPtr window);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the working area of the primary screen according to the given dimensions.
|
||||
/// </summary>
|
||||
/// <exception cref="System.ComponentModel.Win32Exception">
|
||||
/// If the working area could not be set.
|
||||
/// </exception>
|
||||
void SetWorkingArea(RECT bounds);
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters a previously registered system event.
|
||||
/// </summary>
|
||||
/// <exception cref="System.ComponentModel.Win32Exception">
|
||||
/// If the event hook could not be successfully removed.
|
||||
/// </exception>
|
||||
void UnregisterSystemEvent(IntPtr handle);
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SafeExamBrowser.WindowsApi.Types
|
||||
namespace SafeExamBrowser.Contracts.WindowsApi.Types
|
||||
{
|
||||
/// <summary>
|
||||
/// See https://msdn.microsoft.com/en-us/library/windows/desktop/dd162897(v=vs.85).aspx.
|
|
@ -16,27 +16,36 @@ namespace SafeExamBrowser.Core.Behaviour
|
|||
{
|
||||
public class EventController : IEventController
|
||||
{
|
||||
private ILogger logger;
|
||||
private IProcessMonitor processMonitor;
|
||||
private ITaskbar taskbar;
|
||||
private IWindowMonitor windowMonitor;
|
||||
private IWorkingArea workingArea;
|
||||
private ILogger logger;
|
||||
|
||||
public EventController(ILogger logger, IProcessMonitor processMonitor, ITaskbar taskbar, IWorkingArea workingArea)
|
||||
public EventController(
|
||||
ILogger logger,
|
||||
IProcessMonitor processMonitor,
|
||||
ITaskbar taskbar,
|
||||
IWindowMonitor windowMonitor,
|
||||
IWorkingArea workingArea)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.processMonitor = processMonitor;
|
||||
this.taskbar = taskbar;
|
||||
this.windowMonitor = windowMonitor;
|
||||
this.workingArea = workingArea;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
processMonitor.ExplorerStarted += ProcessMonitor_ExplorerStarted;
|
||||
windowMonitor.WindowChanged += processMonitor.OnWindowChanged;
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
processMonitor.ExplorerStarted -= ProcessMonitor_ExplorerStarted;
|
||||
windowMonitor.WindowChanged -= processMonitor.OnWindowChanged;
|
||||
}
|
||||
|
||||
private void ProcessMonitor_ExplorerStarted()
|
||||
|
|
|
@ -14,20 +14,63 @@ using System.Management;
|
|||
using System.Threading;
|
||||
using SafeExamBrowser.Contracts.Logging;
|
||||
using SafeExamBrowser.Contracts.Monitoring;
|
||||
using SafeExamBrowser.WindowsApi;
|
||||
using SafeExamBrowser.Contracts.WindowsApi;
|
||||
|
||||
namespace SafeExamBrowser.Monitoring.Processes
|
||||
{
|
||||
public class ProcessMonitor : IProcessMonitor
|
||||
{
|
||||
private ILogger logger;
|
||||
private INativeMethods nativeMethods;
|
||||
private ManagementEventWatcher explorerWatcher;
|
||||
|
||||
public event ExplorerStartedHandler ExplorerStarted;
|
||||
|
||||
public ProcessMonitor(ILogger logger)
|
||||
public ProcessMonitor(ILogger logger, INativeMethods nativeMethods)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.nativeMethods = nativeMethods;
|
||||
}
|
||||
|
||||
public void CloseExplorerShell()
|
||||
{
|
||||
var processId = nativeMethods.GetShellProcessId();
|
||||
var explorerProcesses = Process.GetProcessesByName("explorer");
|
||||
var shellProcess = explorerProcesses.FirstOrDefault(p => p.Id == processId);
|
||||
|
||||
if (shellProcess != null)
|
||||
{
|
||||
logger.Info($"Found explorer shell processes with PID = {processId}. Sending close message...");
|
||||
|
||||
nativeMethods.PostCloseMessageToShell();
|
||||
|
||||
while (!shellProcess.HasExited)
|
||||
{
|
||||
shellProcess.Refresh();
|
||||
Thread.Sleep(20);
|
||||
}
|
||||
|
||||
logger.Info($"Successfully terminated explorer shell process with PID = {processId}.");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Info("The explorer shell seems to already be terminated. Skipping this step...");
|
||||
}
|
||||
}
|
||||
|
||||
public void OnWindowChanged(IntPtr window, out bool hide)
|
||||
{
|
||||
var processId = nativeMethods.GetProcessIdFor(window);
|
||||
var process = Process.GetProcessById(Convert.ToInt32(processId));
|
||||
|
||||
if (process != null)
|
||||
{
|
||||
hide = process.ProcessName != "SafeExamBrowser";
|
||||
}
|
||||
else
|
||||
{
|
||||
hide = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void StartExplorerShell()
|
||||
|
@ -41,7 +84,7 @@ namespace SafeExamBrowser.Monitoring.Processes
|
|||
process.StartInfo.FileName = explorerPath;
|
||||
process.Start();
|
||||
|
||||
while (User32.GetShellWindowHandle() == IntPtr.Zero)
|
||||
while (nativeMethods.GetShellWindowHandle() == IntPtr.Zero)
|
||||
{
|
||||
Thread.Sleep(20);
|
||||
}
|
||||
|
@ -56,36 +99,14 @@ namespace SafeExamBrowser.Monitoring.Processes
|
|||
explorerWatcher = new ManagementEventWatcher(@"\\.\root\CIMV2", GetQueryFor("explorer.exe"));
|
||||
explorerWatcher.EventArrived += new EventArrivedEventHandler(ExplorerWatcher_EventArrived);
|
||||
explorerWatcher.Start();
|
||||
|
||||
logger.Info("Started monitoring process 'explorer.exe'.");
|
||||
}
|
||||
|
||||
public void StopMonitoringExplorer()
|
||||
{
|
||||
explorerWatcher?.Stop();
|
||||
}
|
||||
|
||||
public void CloseExplorerShell()
|
||||
{
|
||||
var processId = User32.GetShellProcessId();
|
||||
var explorerProcesses = Process.GetProcessesByName("explorer");
|
||||
var shellProcess = explorerProcesses.FirstOrDefault(p => p.Id == processId);
|
||||
|
||||
if (shellProcess != null)
|
||||
{
|
||||
logger.Info($"Found explorer shell processes with PID = {processId}. Sending close message...");
|
||||
User32.PostCloseMessageToShell();
|
||||
|
||||
while (!shellProcess.HasExited)
|
||||
{
|
||||
shellProcess.Refresh();
|
||||
Thread.Sleep(20);
|
||||
}
|
||||
|
||||
logger.Info($"Successfully terminated explorer shell process with PID = {processId}.");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Info("The explorer shell seems to already be terminated. Skipping this step...");
|
||||
}
|
||||
logger.Info("Stopped monitoring 'explorer.exe'.");
|
||||
}
|
||||
|
||||
private void ExplorerWatcher_EventArrived(object sender, EventArrivedEventArgs e)
|
||||
|
|
|
@ -68,10 +68,6 @@
|
|||
<Project>{47da5933-bef8-4729-94e6-abde2db12262}</Project>
|
||||
<Name>SafeExamBrowser.Contracts</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\SafeExamBrowser.WindowsApi\SafeExamBrowser.WindowsApi.csproj">
|
||||
<Project>{73724659-4150-4792-a94e-42f5f3c1b696}</Project>
|
||||
<Name>SafeExamBrowser.WindowsApi</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Keyboard\" />
|
||||
|
|
|
@ -10,30 +10,36 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using SafeExamBrowser.Contracts.Logging;
|
||||
using SafeExamBrowser.Contracts.Monitoring;
|
||||
using SafeExamBrowser.WindowsApi;
|
||||
using SafeExamBrowser.Contracts.WindowsApi;
|
||||
|
||||
namespace SafeExamBrowser.Monitoring.Windows
|
||||
{
|
||||
public class WindowMonitor : IWindowMonitor
|
||||
{
|
||||
private IntPtr captureStartHookHandle;
|
||||
private IntPtr foregroundHookHandle;
|
||||
private ILogger logger;
|
||||
private IList<Window> minimizedWindows = new List<Window>();
|
||||
private INativeMethods nativeMethods;
|
||||
|
||||
public WindowMonitor(ILogger logger)
|
||||
public event WindowChangedHandler WindowChanged;
|
||||
|
||||
public WindowMonitor(ILogger logger, INativeMethods nativeMethods)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.nativeMethods = nativeMethods;
|
||||
}
|
||||
|
||||
public void HideAllWindows()
|
||||
{
|
||||
logger.Info("Saving windows to be minimized...");
|
||||
|
||||
foreach (var handle in User32.GetOpenWindows())
|
||||
foreach (var handle in nativeMethods.GetOpenWindows())
|
||||
{
|
||||
var window = new Window
|
||||
{
|
||||
Handle = handle,
|
||||
Title = User32.GetWindowTitle(handle)
|
||||
Title = nativeMethods.GetWindowTitle(handle)
|
||||
};
|
||||
|
||||
minimizedWindows.Add(window);
|
||||
|
@ -41,7 +47,7 @@ namespace SafeExamBrowser.Monitoring.Windows
|
|||
}
|
||||
|
||||
logger.Info("Minimizing all open windows...");
|
||||
User32.MinimizeAllOpenWindows();
|
||||
nativeMethods.MinimizeAllOpenWindows();
|
||||
logger.Info("Open windows successfully minimized.");
|
||||
}
|
||||
|
||||
|
@ -51,7 +57,7 @@ namespace SafeExamBrowser.Monitoring.Windows
|
|||
|
||||
foreach (var window in minimizedWindows)
|
||||
{
|
||||
User32.RestoreWindow(window.Handle);
|
||||
nativeMethods.RestoreWindow(window.Handle);
|
||||
logger.Info($"Restored window '{window.Title}' with handle = {window.Handle}.");
|
||||
}
|
||||
|
||||
|
@ -60,12 +66,42 @@ namespace SafeExamBrowser.Monitoring.Windows
|
|||
|
||||
public void StartMonitoringWindows()
|
||||
{
|
||||
// TODO
|
||||
captureStartHookHandle = nativeMethods.RegisterSystemCaptureStartEvent(OnWindowChanged);
|
||||
logger.Info($"Registered system capture start event with handle = {captureStartHookHandle}.");
|
||||
|
||||
foregroundHookHandle = nativeMethods.RegisterSystemForegroundEvent(OnWindowChanged);
|
||||
logger.Info($"Registered system foreground event with handle = {foregroundHookHandle}.");
|
||||
}
|
||||
|
||||
public void StopMonitoringWindows()
|
||||
{
|
||||
// TODO
|
||||
if (captureStartHookHandle != IntPtr.Zero)
|
||||
{
|
||||
nativeMethods.UnregisterSystemEvent(captureStartHookHandle);
|
||||
logger.Info($"Unregistered system capture start event with handle = {captureStartHookHandle}.");
|
||||
}
|
||||
|
||||
if (foregroundHookHandle != IntPtr.Zero)
|
||||
{
|
||||
nativeMethods.UnregisterSystemEvent(foregroundHookHandle);
|
||||
logger.Info($"Unregistered system foreground event with handle = {foregroundHookHandle}.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnWindowChanged(IntPtr window)
|
||||
{
|
||||
if (WindowChanged != null)
|
||||
{
|
||||
WindowChanged.Invoke(window, out bool hide);
|
||||
|
||||
if (hide)
|
||||
{
|
||||
var title = nativeMethods.GetWindowTitle(window);
|
||||
|
||||
nativeMethods.HideWindow(window);
|
||||
logger.Info($"Hid window '{title}' with handle = {window}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct Window
|
||||
|
|
|
@ -8,13 +8,45 @@
|
|||
|
||||
namespace SafeExamBrowser.WindowsApi.Constants
|
||||
{
|
||||
static class Constant
|
||||
internal static class Constant
|
||||
{
|
||||
internal const int WM_COMMAND = 0x111;
|
||||
/// <summary>
|
||||
/// A window has received mouse capture. This event is sent by the system, never by servers.
|
||||
///
|
||||
/// See https://msdn.microsoft.com/en-us/library/windows/desktop/dd318066(v=vs.85).aspx.
|
||||
/// </summary>
|
||||
internal const uint EVENT_SYSTEM_CAPTURESTART = 0x8;
|
||||
|
||||
/// <summary>
|
||||
/// The foreground window has changed. The system sends this event even if the foreground window has changed to another window in
|
||||
/// the same thread. Server applications never send this event.
|
||||
/// For this event, the WinEventProc callback function's hwnd parameter is the handle to the window that is in the foreground, the
|
||||
/// idObject parameter is OBJID_WINDOW, and the idChild parameter is CHILDID_SELF.
|
||||
///
|
||||
/// See https://msdn.microsoft.com/en-us/library/windows/desktop/dd318066(v=vs.85).aspx.
|
||||
/// </summary>
|
||||
internal const uint EVENT_SYSTEM_FOREGROUND = 0x3;
|
||||
|
||||
/// <summary>
|
||||
/// Minimize all open windows.
|
||||
/// </summary>
|
||||
internal const int MIN_ALL = 419;
|
||||
|
||||
/// <summary>
|
||||
/// The callback function is not mapped into the address space of the process that generates the event. Because the hook function
|
||||
/// is called across process boundaries, the system must queue events. Although this method is asynchronous, events are guaranteed
|
||||
/// to be in sequential order.
|
||||
///
|
||||
/// See https://msdn.microsoft.com/en-us/library/windows/desktop/dd373640(v=vs.85).aspx.
|
||||
/// </summary>
|
||||
internal const uint WINEVENT_OUTOFCONTEXT = 0x0;
|
||||
|
||||
/// <summary>
|
||||
/// Sent when the user selects a command item from a menu, when a control sends a notification message to its parent window, or
|
||||
/// when an accelerator keystroke is translated.
|
||||
///
|
||||
/// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms647591(v=vs.85).aspx.
|
||||
/// </summary>
|
||||
internal const int WM_COMMAND = 0x111;
|
||||
}
|
||||
}
|
||||
|
|
184
SafeExamBrowser.WindowsApi/NativeMethods.cs
Normal file
184
SafeExamBrowser.WindowsApi/NativeMethods.cs
Normal file
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
* Copyright (c) 2017 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;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using SafeExamBrowser.Contracts.WindowsApi;
|
||||
using SafeExamBrowser.Contracts.WindowsApi.Types;
|
||||
using SafeExamBrowser.WindowsApi.Constants;
|
||||
|
||||
namespace SafeExamBrowser.WindowsApi
|
||||
{
|
||||
public class NativeMethods : INativeMethods
|
||||
{
|
||||
private ConcurrentDictionary<IntPtr, WinEventDelegate> EventDelegates = new ConcurrentDictionary<IntPtr, WinEventDelegate>();
|
||||
|
||||
public IEnumerable<IntPtr> GetOpenWindows()
|
||||
{
|
||||
var windows = new List<IntPtr>();
|
||||
var success = User32.EnumWindows(delegate (IntPtr hWnd, IntPtr lParam)
|
||||
{
|
||||
if (hWnd != GetShellWindowHandle() && User32.IsWindowVisible(hWnd) && User32.GetWindowTextLength(hWnd) > 0)
|
||||
{
|
||||
windows.Add(hWnd);
|
||||
}
|
||||
|
||||
return true;
|
||||
}, IntPtr.Zero);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
}
|
||||
|
||||
return windows;
|
||||
}
|
||||
|
||||
public uint GetProcessIdFor(IntPtr window)
|
||||
{
|
||||
User32.GetWindowThreadProcessId(window, out uint processId);
|
||||
|
||||
return processId;
|
||||
}
|
||||
|
||||
public IntPtr GetShellWindowHandle()
|
||||
{
|
||||
return User32.FindWindow("Shell_TrayWnd", null);
|
||||
}
|
||||
|
||||
public uint GetShellProcessId()
|
||||
{
|
||||
var handle = GetShellWindowHandle();
|
||||
var threadId = User32.GetWindowThreadProcessId(handle, out uint processId);
|
||||
|
||||
return processId;
|
||||
}
|
||||
|
||||
public string GetWindowTitle(IntPtr window)
|
||||
{
|
||||
var length = User32.GetWindowTextLength(window);
|
||||
|
||||
if (length > 0)
|
||||
{
|
||||
var builder = new StringBuilder(length);
|
||||
|
||||
User32.GetWindowText(window, builder, length + 1);
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public RECT GetWorkingArea()
|
||||
{
|
||||
var workingArea = new RECT();
|
||||
var success = User32.SystemParametersInfo(SPI.GETWORKAREA, 0, ref workingArea, SPIF.NONE);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
}
|
||||
|
||||
return workingArea;
|
||||
}
|
||||
|
||||
public void HideWindow(IntPtr window)
|
||||
{
|
||||
User32.ShowWindow(window, (int)ShowWindowCommand.Hide);
|
||||
}
|
||||
|
||||
public void MinimizeAllOpenWindows()
|
||||
{
|
||||
var handle = GetShellWindowHandle();
|
||||
|
||||
User32.SendMessage(handle, Constant.WM_COMMAND, (IntPtr)Constant.MIN_ALL, IntPtr.Zero);
|
||||
}
|
||||
|
||||
public void PostCloseMessageToShell()
|
||||
{
|
||||
// NOTE: The close message 0x5B4 posted to the shell is undocumented and not officially supported:
|
||||
// https://stackoverflow.com/questions/5689904/gracefully-exit-explorer-programmatically/5705965#5705965
|
||||
|
||||
var handle = GetShellWindowHandle();
|
||||
var success = User32.PostMessage(handle, 0x5B4, IntPtr.Zero, IntPtr.Zero);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
}
|
||||
}
|
||||
|
||||
public IntPtr RegisterSystemForegroundEvent(Action<IntPtr> callback)
|
||||
{
|
||||
WinEventDelegate eventProc = (IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime) =>
|
||||
{
|
||||
callback(hwnd);
|
||||
};
|
||||
|
||||
var handle = User32.SetWinEventHook(Constant.EVENT_SYSTEM_FOREGROUND, Constant.EVENT_SYSTEM_FOREGROUND, IntPtr.Zero, eventProc, 0, 0, Constant.WINEVENT_OUTOFCONTEXT);
|
||||
|
||||
// IMORTANT:
|
||||
// Ensures that the callback does not get garbage collected prematurely, as it will be passed to unmanaged code!
|
||||
// Not doing so will result in a <c>CallbackOnCollectedDelegate</c> error and subsequent application crash.
|
||||
EventDelegates[handle] = eventProc;
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
public IntPtr RegisterSystemCaptureStartEvent(Action<IntPtr> callback)
|
||||
{
|
||||
WinEventDelegate eventProc = (IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime) =>
|
||||
{
|
||||
callback(hwnd);
|
||||
};
|
||||
|
||||
var handle = User32.SetWinEventHook(Constant.EVENT_SYSTEM_CAPTURESTART, Constant.EVENT_SYSTEM_CAPTURESTART, IntPtr.Zero, eventProc, 0, 0, Constant.WINEVENT_OUTOFCONTEXT);
|
||||
|
||||
// IMORTANT:
|
||||
// Ensures that the callback does not get garbage collected prematurely, as it will be passed to unmanaged code!
|
||||
// Not doing so will result in a <c>CallbackOnCollectedDelegate</c> error and subsequent application crash.
|
||||
EventDelegates[handle] = eventProc;
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
public void RestoreWindow(IntPtr window)
|
||||
{
|
||||
User32.ShowWindow(window, (int)ShowWindowCommand.Restore);
|
||||
}
|
||||
|
||||
public void SetWorkingArea(RECT bounds)
|
||||
{
|
||||
var success = User32.SystemParametersInfo(SPI.SETWORKAREA, 0, ref bounds, SPIF.UPDATEANDCHANGE);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
}
|
||||
}
|
||||
|
||||
public void UnregisterSystemEvent(IntPtr handle)
|
||||
{
|
||||
var success = User32.UnhookWinEvent(handle);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
}
|
||||
else
|
||||
{
|
||||
EventDelegates.TryRemove(handle, out WinEventDelegate d);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -60,11 +60,17 @@
|
|||
<ItemGroup>
|
||||
<Compile Include="Constants\Constant.cs" />
|
||||
<Compile Include="Constants\ShowWindowCommand.cs" />
|
||||
<Compile Include="NativeMethods.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Constants\SPI.cs" />
|
||||
<Compile Include="Constants\SPIF.cs" />
|
||||
<Compile Include="Types\RECT.cs" />
|
||||
<Compile Include="User32.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SafeExamBrowser.Contracts\SafeExamBrowser.Contracts.csproj">
|
||||
<Project>{47DA5933-BEF8-4729-94E6-ABDE2DB12262}</Project>
|
||||
<Name>SafeExamBrowser.Contracts</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
|
@ -7,193 +7,60 @@
|
|||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using SafeExamBrowser.Contracts.WindowsApi.Types;
|
||||
using SafeExamBrowser.WindowsApi.Constants;
|
||||
using SafeExamBrowser.WindowsApi.Types;
|
||||
|
||||
namespace SafeExamBrowser.WindowsApi
|
||||
{
|
||||
internal delegate bool EnumWindowsDelegate(IntPtr hWnd, IntPtr lParam);
|
||||
internal delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);
|
||||
|
||||
/// <summary>
|
||||
/// Provides access to the native Windows API exposed by <c>user32.dll</c>.
|
||||
/// </summary>
|
||||
public static class User32
|
||||
internal static class User32
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves a collection of handles to all currently open (i.e. visible) windows.
|
||||
/// </summary>
|
||||
public static IEnumerable<IntPtr> GetOpenWindows()
|
||||
{
|
||||
var windows = new List<IntPtr>();
|
||||
var success = EnumWindows(delegate (IntPtr hWnd, IntPtr lParam)
|
||||
{
|
||||
if (hWnd != GetShellWindowHandle() && IsWindowVisible(hWnd) && GetWindowTextLength(hWnd) > 0)
|
||||
{
|
||||
windows.Add(hWnd);
|
||||
}
|
||||
|
||||
return true;
|
||||
}, IntPtr.Zero);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
}
|
||||
|
||||
return windows;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a window handle to the Windows taskbar. Returns <c>IntPtr.Zero</c>
|
||||
/// if the taskbar could not be found (i.e. if it isn't running).
|
||||
/// </summary>
|
||||
public static IntPtr GetShellWindowHandle()
|
||||
{
|
||||
return FindWindow("Shell_TrayWnd", null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the process ID of the main Windows explorer instance controlling
|
||||
/// desktop and taskbar or <c>0</c>, if the process isn't running.
|
||||
/// </summary>
|
||||
public static uint GetShellProcessId()
|
||||
{
|
||||
var handle = GetShellWindowHandle();
|
||||
var threadId = GetWindowThreadProcessId(handle, out uint processId);
|
||||
|
||||
return processId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the title of the specified window, or an empty string, if the
|
||||
/// given window does not have a title.
|
||||
/// </summary>
|
||||
public static string GetWindowTitle(IntPtr window)
|
||||
{
|
||||
var length = GetWindowTextLength(window);
|
||||
|
||||
if (length > 0)
|
||||
{
|
||||
var builder = new StringBuilder(length);
|
||||
|
||||
GetWindowText(window, builder, length + 1);
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the currently configured working area of the primary screen.
|
||||
/// </summary>
|
||||
/// <exception cref="System.ComponentModel.Win32Exception">
|
||||
/// If the working area could not be retrieved.
|
||||
/// </exception>
|
||||
public static RECT GetWorkingArea()
|
||||
{
|
||||
var workingArea = new RECT();
|
||||
var success = SystemParametersInfo(SPI.GETWORKAREA, 0, ref workingArea, SPIF.NONE);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
}
|
||||
|
||||
return workingArea;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimizes all open windows.
|
||||
/// </summary>
|
||||
public static void MinimizeAllOpenWindows()
|
||||
{
|
||||
var handle = GetShellWindowHandle();
|
||||
|
||||
SendMessage(handle, Constant.WM_COMMAND, (IntPtr) Constant.MIN_ALL, IntPtr.Zero);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Instructs the main Windows explorer process to shut down.
|
||||
/// </summary>
|
||||
/// <exception cref="System.ComponentModel.Win32Exception">
|
||||
/// If the messge could not be successfully posted. Does not apply if the process isn't running!
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// The close message <c>0x5B4</c> posted to the shell is undocumented and not officially supported:
|
||||
/// https://stackoverflow.com/questions/5689904/gracefully-exit-explorer-programmatically/5705965#5705965
|
||||
/// </remarks>
|
||||
public static void PostCloseMessageToShell()
|
||||
{
|
||||
var handle = GetShellWindowHandle();
|
||||
var success = PostMessage(handle, 0x5B4, IntPtr.Zero, IntPtr.Zero);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores the specified window to its original size and position.
|
||||
/// </summary>
|
||||
public static void RestoreWindow(IntPtr window)
|
||||
{
|
||||
ShowWindow(window, (int) ShowWindowCommand.Restore);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the working area of the primary screen according to the given dimensions.
|
||||
/// </summary>
|
||||
/// <exception cref="System.ComponentModel.Win32Exception">
|
||||
/// If the working area could not be set.
|
||||
/// </exception>
|
||||
public static void SetWorkingArea(RECT workingArea)
|
||||
{
|
||||
var success = SystemParametersInfo(SPI.SETWORKAREA, 0, ref workingArea, SPIF.UPDATEANDCHANGE);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
}
|
||||
}
|
||||
|
||||
private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool EnumWindows(EnumWindowsProc enumProc, IntPtr lParam);
|
||||
internal static extern bool EnumWindows(EnumWindowsDelegate enumProc, IntPtr lParam);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
|
||||
internal static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern int GetWindowText(IntPtr hWnd, StringBuilder strText, int maxCount);
|
||||
internal static extern int GetWindowText(IntPtr hWnd, StringBuilder strText, int maxCount);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern int GetWindowTextLength(IntPtr hWnd);
|
||||
internal static extern int GetWindowTextLength(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
|
||||
internal static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool IsWindowVisible(IntPtr hWnd);
|
||||
internal static extern bool IsWindowVisible(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool PostMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true, EntryPoint = "SendMessage")]
|
||||
private static extern IntPtr SendMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
|
||||
internal static extern bool PostMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
||||
internal static extern IntPtr SendMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
internal static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool SystemParametersInfo(SPI uiAction, uint uiParam, ref RECT pvParam, SPIF fWinIni);
|
||||
internal static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static extern bool SystemParametersInfo(SPI uiAction, uint uiParam, ref RECT pvParam, SPIF fWinIni);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
internal static extern bool UnhookWinEvent(IntPtr hWinEventHook);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ using SafeExamBrowser.Contracts.I18n;
|
|||
using SafeExamBrowser.Contracts.Logging;
|
||||
using SafeExamBrowser.Contracts.Monitoring;
|
||||
using SafeExamBrowser.Contracts.UserInterface;
|
||||
using SafeExamBrowser.Contracts.WindowsApi;
|
||||
using SafeExamBrowser.Core.Behaviour;
|
||||
using SafeExamBrowser.Core.Behaviour.Operations;
|
||||
using SafeExamBrowser.Core.I18n;
|
||||
|
@ -22,6 +23,7 @@ using SafeExamBrowser.Core.Logging;
|
|||
using SafeExamBrowser.Monitoring.Processes;
|
||||
using SafeExamBrowser.Monitoring.Windows;
|
||||
using SafeExamBrowser.UserInterface;
|
||||
using SafeExamBrowser.WindowsApi;
|
||||
|
||||
namespace SafeExamBrowser
|
||||
{
|
||||
|
@ -31,6 +33,7 @@ namespace SafeExamBrowser
|
|||
private IApplicationInfo browserInfo;
|
||||
private IEventController eventController;
|
||||
private ILogger logger;
|
||||
private INativeMethods nativeMethods;
|
||||
private INotificationInfo aboutInfo;
|
||||
private IProcessMonitor processMonitor;
|
||||
private ISettings settings;
|
||||
|
@ -49,6 +52,7 @@ namespace SafeExamBrowser
|
|||
{
|
||||
browserInfo = new BrowserApplicationInfo();
|
||||
logger = new Logger();
|
||||
nativeMethods = new NativeMethods();
|
||||
settings = new Settings();
|
||||
Taskbar = new Taskbar();
|
||||
textResource = new XmlTextResource();
|
||||
|
@ -59,10 +63,10 @@ namespace SafeExamBrowser
|
|||
text = new Text(textResource);
|
||||
aboutInfo = new AboutNotificationInfo(text);
|
||||
browserController = new BrowserApplicationController(settings, uiFactory);
|
||||
processMonitor = new ProcessMonitor(new ModuleLogger(logger, typeof(ProcessMonitor)));
|
||||
windowMonitor = new WindowMonitor(new ModuleLogger(logger, typeof(WindowMonitor)));
|
||||
workingArea = new WorkingArea(new ModuleLogger(logger, typeof(WorkingArea)));
|
||||
eventController = new EventController(new ModuleLogger(logger, typeof(EventController)), processMonitor, Taskbar, workingArea);
|
||||
processMonitor = new ProcessMonitor(new ModuleLogger(logger, typeof(ProcessMonitor)), nativeMethods);
|
||||
windowMonitor = new WindowMonitor(new ModuleLogger(logger, typeof(WindowMonitor)), nativeMethods);
|
||||
workingArea = new WorkingArea(new ModuleLogger(logger, typeof(WorkingArea)), nativeMethods);
|
||||
eventController = new EventController(new ModuleLogger(logger, typeof(EventController)), processMonitor, Taskbar, windowMonitor, workingArea);
|
||||
ShutdownController = new ShutdownController(logger, settings, text, uiFactory);
|
||||
StartupController = new StartupController(logger, settings, text, uiFactory);
|
||||
|
||||
|
|
|
@ -143,6 +143,10 @@
|
|||
<Project>{e1be031a-4354-41e7-83e8-843ded4489ff}</Project>
|
||||
<Name>SafeExamBrowser.UserInterface</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\SafeExamBrowser.WindowsApi\SafeExamBrowser.WindowsApi.csproj">
|
||||
<Project>{73724659-4150-4792-A94E-42F5F3C1B696}</Project>
|
||||
<Name>SafeExamBrowser.WindowsApi</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<BootstrapperPackage Include=".NETFramework,Version=v4.5.2">
|
||||
|
|
Loading…
Reference in a new issue