Completed basic implementation of window and process monitoring.

This commit is contained in:
dbuechel 2017-07-27 11:46:31 +02:00
parent 9ab04ecc77
commit 25105d61b1
19 changed files with 516 additions and 223 deletions

View file

@ -53,6 +53,11 @@ namespace SafeExamBrowser.Browser
public void Terminate()
{
foreach (var instance in instances)
{
instance.Window.Close();
}
Cef.Shutdown();
}

View file

@ -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>

View file

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

View file

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

View file

@ -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>

View file

@ -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>

View file

@ -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>

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

View file

@ -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.

View file

@ -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()

View file

@ -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)

View file

@ -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\" />

View file

@ -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

View file

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

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

View file

@ -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>

View file

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

View file

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

View file

@ -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">