From 25105d61b1b71ba2ae58d0e37f3d1e845f4cd911 Mon Sep 17 00:00:00 2001 From: dbuechel Date: Thu, 27 Jul 2017 11:46:31 +0200 Subject: [PATCH] Completed basic implementation of window and process monitoring. --- .../BrowserApplicationController.cs | 5 + .../SafeExamBrowser.Configuration.csproj | 4 - SafeExamBrowser.Configuration/WorkingArea.cs | 16 +- .../Monitoring/IProcessMonitor.cs | 18 +- .../Monitoring/IWindowMonitor.cs | 9 + .../SafeExamBrowser.Contracts.csproj | 2 + .../UserInterface/IWindow.cs | 5 + .../WindowsApi/INativeMethods.cs | 107 ++++++++++ .../WindowsApi}/Types/RECT.cs | 2 +- .../Behaviour/EventController.cs | 13 +- .../Processes/ProcessMonitor.cs | 77 +++++--- .../SafeExamBrowser.Monitoring.csproj | 4 - .../Windows/WindowMonitor.cs | 52 ++++- .../Constants/Constant.cs | 36 +++- SafeExamBrowser.WindowsApi/NativeMethods.cs | 184 ++++++++++++++++++ .../SafeExamBrowser.WindowsApi.csproj | 8 +- SafeExamBrowser.WindowsApi/User32.cs | 181 +++-------------- SafeExamBrowser/CompositionRoot.cs | 12 +- SafeExamBrowser/SafeExamBrowser.csproj | 4 + 19 files changed, 516 insertions(+), 223 deletions(-) create mode 100644 SafeExamBrowser.Contracts/WindowsApi/INativeMethods.cs rename {SafeExamBrowser.WindowsApi => SafeExamBrowser.Contracts/WindowsApi}/Types/RECT.cs (91%) create mode 100644 SafeExamBrowser.WindowsApi/NativeMethods.cs diff --git a/SafeExamBrowser.Browser/BrowserApplicationController.cs b/SafeExamBrowser.Browser/BrowserApplicationController.cs index 6683b61b..13eb9a6c 100644 --- a/SafeExamBrowser.Browser/BrowserApplicationController.cs +++ b/SafeExamBrowser.Browser/BrowserApplicationController.cs @@ -53,6 +53,11 @@ namespace SafeExamBrowser.Browser public void Terminate() { + foreach (var instance in instances) + { + instance.Window.Close(); + } + Cef.Shutdown(); } diff --git a/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj b/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj index f6ff08aa..8ccf808b 100644 --- a/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj +++ b/SafeExamBrowser.Configuration/SafeExamBrowser.Configuration.csproj @@ -71,10 +71,6 @@ {47DA5933-BEF8-4729-94E6-ABDE2DB12262} SafeExamBrowser.Contracts - - {73724659-4150-4792-a94e-42f5f3c1b696} - SafeExamBrowser.WindowsApi - \ No newline at end of file diff --git a/SafeExamBrowser.Configuration/WorkingArea.cs b/SafeExamBrowser.Configuration/WorkingArea.cs index 74c53ba9..804b1b03 100644 --- a/SafeExamBrowser.Configuration/WorkingArea.cs +++ b/SafeExamBrowser.Configuration/WorkingArea.cs @@ -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); } } diff --git a/SafeExamBrowser.Contracts/Monitoring/IProcessMonitor.cs b/SafeExamBrowser.Contracts/Monitoring/IProcessMonitor.cs index 94157ca8..710acc68 100644 --- a/SafeExamBrowser.Contracts/Monitoring/IProcessMonitor.cs +++ b/SafeExamBrowser.Contracts/Monitoring/IProcessMonitor.cs @@ -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 /// event ExplorerStartedHandler ExplorerStarted; + /// + /// Terminates the Windows explorer shell, i.e. the taskbar. + /// + void CloseExplorerShell(); + + /// + /// Performs a check whether the process associated to the given window is allowed, + /// i.e. whether the specified window should be hidden. + /// + void OnWindowChanged(IntPtr window, out bool hide); + /// /// Starts a new instance of the Windows explorer shell. /// @@ -33,10 +46,5 @@ namespace SafeExamBrowser.Contracts.Monitoring /// Stops monitoring the Windows explorer. /// void StopMonitoringExplorer(); - - /// - /// Terminates the Windows explorer shell, i.e. the taskbar. - /// - void CloseExplorerShell(); } } diff --git a/SafeExamBrowser.Contracts/Monitoring/IWindowMonitor.cs b/SafeExamBrowser.Contracts/Monitoring/IWindowMonitor.cs index 53f66c4f..8f79d0f1 100644 --- a/SafeExamBrowser.Contracts/Monitoring/IWindowMonitor.cs +++ b/SafeExamBrowser.Contracts/Monitoring/IWindowMonitor.cs @@ -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 { + /// + /// Event fired when the window monitor observes that the foreground window has changed. + /// + event WindowChangedHandler WindowChanged; + /// /// Hides all currently opened windows. /// diff --git a/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj b/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj index ddd82773..b34a3b01 100644 --- a/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj +++ b/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj @@ -96,6 +96,8 @@ + + \ No newline at end of file diff --git a/SafeExamBrowser.Contracts/UserInterface/IWindow.cs b/SafeExamBrowser.Contracts/UserInterface/IWindow.cs index 01869079..b6423f7e 100644 --- a/SafeExamBrowser.Contracts/UserInterface/IWindow.cs +++ b/SafeExamBrowser.Contracts/UserInterface/IWindow.cs @@ -15,6 +15,11 @@ namespace SafeExamBrowser.Contracts.UserInterface /// void BringToForeground(); + /// + /// Closes the window. + /// + void Close(); + /// /// Shows the window to the user. /// diff --git a/SafeExamBrowser.Contracts/WindowsApi/INativeMethods.cs b/SafeExamBrowser.Contracts/WindowsApi/INativeMethods.cs new file mode 100644 index 00000000..926b1828 --- /dev/null +++ b/SafeExamBrowser.Contracts/WindowsApi/INativeMethods.cs @@ -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 + { + /// + /// Retrieves a collection of handles to all currently open (i.e. visible) windows. + /// + /// + /// If the open windows could not be retrieved. + /// + IEnumerable GetOpenWindows(); + + /// + /// Retrieves the process identifier for the specified window handle. + /// + uint GetProcessIdFor(IntPtr window); + + /// + /// Retrieves a window handle to the Windows taskbar. Returns IntPtr.Zero + /// if the taskbar could not be found (i.e. if it isn't running). + /// + IntPtr GetShellWindowHandle(); + + /// + /// Retrieves the process ID of the main Windows explorer instance controlling + /// desktop and taskbar or 0, if the process isn't running. + /// + uint GetShellProcessId(); + + /// + /// Retrieves the title of the specified window, or an empty string, if the + /// given window does not have a title. + /// + string GetWindowTitle(IntPtr window); + + /// + /// Retrieves the currently configured working area of the primary screen. + /// + /// + /// If the working area could not be retrieved. + /// + RECT GetWorkingArea(); + + /// + /// Hides the given window. + /// + void HideWindow(IntPtr window); + + /// + /// Minimizes all open windows. + /// + void MinimizeAllOpenWindows(); + + /// + /// Instructs the main Windows explorer process to shut down. + /// + /// + /// If the message could not be successfully posted. Does not apply if the process isn't running! + /// + void PostCloseMessageToShell(); + + /// + /// 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. + /// + IntPtr RegisterSystemForegroundEvent(Action callback); + + /// + /// 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. + /// + IntPtr RegisterSystemCaptureStartEvent(Action callback); + + /// + /// Restores the specified window to its original size and position. + /// + void RestoreWindow(IntPtr window); + + /// + /// Sets the working area of the primary screen according to the given dimensions. + /// + /// + /// If the working area could not be set. + /// + void SetWorkingArea(RECT bounds); + + /// + /// Unregisters a previously registered system event. + /// + /// + /// If the event hook could not be successfully removed. + /// + void UnregisterSystemEvent(IntPtr handle); + } +} diff --git a/SafeExamBrowser.WindowsApi/Types/RECT.cs b/SafeExamBrowser.Contracts/WindowsApi/Types/RECT.cs similarity index 91% rename from SafeExamBrowser.WindowsApi/Types/RECT.cs rename to SafeExamBrowser.Contracts/WindowsApi/Types/RECT.cs index 44077323..7b883657 100644 --- a/SafeExamBrowser.WindowsApi/Types/RECT.cs +++ b/SafeExamBrowser.Contracts/WindowsApi/Types/RECT.cs @@ -8,7 +8,7 @@ using System.Runtime.InteropServices; -namespace SafeExamBrowser.WindowsApi.Types +namespace SafeExamBrowser.Contracts.WindowsApi.Types { /// /// See https://msdn.microsoft.com/en-us/library/windows/desktop/dd162897(v=vs.85).aspx. diff --git a/SafeExamBrowser.Core/Behaviour/EventController.cs b/SafeExamBrowser.Core/Behaviour/EventController.cs index 3004b473..dadcf1d3 100644 --- a/SafeExamBrowser.Core/Behaviour/EventController.cs +++ b/SafeExamBrowser.Core/Behaviour/EventController.cs @@ -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() diff --git a/SafeExamBrowser.Monitoring/Processes/ProcessMonitor.cs b/SafeExamBrowser.Monitoring/Processes/ProcessMonitor.cs index ca924e80..a415e894 100644 --- a/SafeExamBrowser.Monitoring/Processes/ProcessMonitor.cs +++ b/SafeExamBrowser.Monitoring/Processes/ProcessMonitor.cs @@ -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) diff --git a/SafeExamBrowser.Monitoring/SafeExamBrowser.Monitoring.csproj b/SafeExamBrowser.Monitoring/SafeExamBrowser.Monitoring.csproj index 7c7308ff..4b2e9b30 100644 --- a/SafeExamBrowser.Monitoring/SafeExamBrowser.Monitoring.csproj +++ b/SafeExamBrowser.Monitoring/SafeExamBrowser.Monitoring.csproj @@ -68,10 +68,6 @@ {47da5933-bef8-4729-94e6-abde2db12262} SafeExamBrowser.Contracts - - {73724659-4150-4792-a94e-42f5f3c1b696} - SafeExamBrowser.WindowsApi - diff --git a/SafeExamBrowser.Monitoring/Windows/WindowMonitor.cs b/SafeExamBrowser.Monitoring/Windows/WindowMonitor.cs index ac5df2a6..adc2df70 100644 --- a/SafeExamBrowser.Monitoring/Windows/WindowMonitor.cs +++ b/SafeExamBrowser.Monitoring/Windows/WindowMonitor.cs @@ -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 minimizedWindows = new List(); + 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 diff --git a/SafeExamBrowser.WindowsApi/Constants/Constant.cs b/SafeExamBrowser.WindowsApi/Constants/Constant.cs index 61af4425..9451edba 100644 --- a/SafeExamBrowser.WindowsApi/Constants/Constant.cs +++ b/SafeExamBrowser.WindowsApi/Constants/Constant.cs @@ -8,13 +8,45 @@ namespace SafeExamBrowser.WindowsApi.Constants { - static class Constant + internal static class Constant { - internal const int WM_COMMAND = 0x111; + /// + /// 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. + /// + internal const uint EVENT_SYSTEM_CAPTURESTART = 0x8; + + /// + /// 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. + /// + internal const uint EVENT_SYSTEM_FOREGROUND = 0x3; /// /// Minimize all open windows. /// internal const int MIN_ALL = 419; + + /// + /// 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. + /// + internal const uint WINEVENT_OUTOFCONTEXT = 0x0; + + /// + /// 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. + /// + internal const int WM_COMMAND = 0x111; } } diff --git a/SafeExamBrowser.WindowsApi/NativeMethods.cs b/SafeExamBrowser.WindowsApi/NativeMethods.cs new file mode 100644 index 00000000..7244d2d7 --- /dev/null +++ b/SafeExamBrowser.WindowsApi/NativeMethods.cs @@ -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 EventDelegates = new ConcurrentDictionary(); + + public IEnumerable GetOpenWindows() + { + var windows = new List(); + 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 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 CallbackOnCollectedDelegate error and subsequent application crash. + EventDelegates[handle] = eventProc; + + return handle; + } + + public IntPtr RegisterSystemCaptureStartEvent(Action 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 CallbackOnCollectedDelegate 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); + } + } + } +} diff --git a/SafeExamBrowser.WindowsApi/SafeExamBrowser.WindowsApi.csproj b/SafeExamBrowser.WindowsApi/SafeExamBrowser.WindowsApi.csproj index 514773e0..2d9401db 100644 --- a/SafeExamBrowser.WindowsApi/SafeExamBrowser.WindowsApi.csproj +++ b/SafeExamBrowser.WindowsApi/SafeExamBrowser.WindowsApi.csproj @@ -60,11 +60,17 @@ + - + + + {47DA5933-BEF8-4729-94E6-ABDE2DB12262} + SafeExamBrowser.Contracts + + \ No newline at end of file diff --git a/SafeExamBrowser.WindowsApi/User32.cs b/SafeExamBrowser.WindowsApi/User32.cs index b5562b3a..625001f5 100644 --- a/SafeExamBrowser.WindowsApi/User32.cs +++ b/SafeExamBrowser.WindowsApi/User32.cs @@ -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); + /// /// Provides access to the native Windows API exposed by user32.dll. /// - public static class User32 + internal static class User32 { - /// - /// Retrieves a collection of handles to all currently open (i.e. visible) windows. - /// - public static IEnumerable GetOpenWindows() - { - var windows = new List(); - 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; - } - - /// - /// Retrieves a window handle to the Windows taskbar. Returns IntPtr.Zero - /// if the taskbar could not be found (i.e. if it isn't running). - /// - public static IntPtr GetShellWindowHandle() - { - return FindWindow("Shell_TrayWnd", null); - } - - /// - /// Retrieves the process ID of the main Windows explorer instance controlling - /// desktop and taskbar or 0, if the process isn't running. - /// - public static uint GetShellProcessId() - { - var handle = GetShellWindowHandle(); - var threadId = GetWindowThreadProcessId(handle, out uint processId); - - return processId; - } - - /// - /// Retrieves the title of the specified window, or an empty string, if the - /// given window does not have a title. - /// - 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; - } - - /// - /// Retrieves the currently configured working area of the primary screen. - /// - /// - /// If the working area could not be retrieved. - /// - 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; - } - - /// - /// Minimizes all open windows. - /// - public static void MinimizeAllOpenWindows() - { - var handle = GetShellWindowHandle(); - - SendMessage(handle, Constant.WM_COMMAND, (IntPtr) Constant.MIN_ALL, IntPtr.Zero); - } - - /// - /// Instructs the main Windows explorer process to shut down. - /// - /// - /// If the messge could not be successfully posted. Does not apply if the process isn't running! - /// - /// - /// 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 - /// - public static void PostCloseMessageToShell() - { - var handle = GetShellWindowHandle(); - var success = PostMessage(handle, 0x5B4, IntPtr.Zero, IntPtr.Zero); - - if (!success) - { - throw new Win32Exception(Marshal.GetLastWin32Error()); - } - } - - /// - /// Restores the specified window to its original size and position. - /// - public static void RestoreWindow(IntPtr window) - { - ShowWindow(window, (int) ShowWindowCommand.Restore); - } - - /// - /// Sets the working area of the primary screen according to the given dimensions. - /// - /// - /// If the working area could not be set. - /// - 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); } } diff --git a/SafeExamBrowser/CompositionRoot.cs b/SafeExamBrowser/CompositionRoot.cs index d9c61164..7ad71073 100644 --- a/SafeExamBrowser/CompositionRoot.cs +++ b/SafeExamBrowser/CompositionRoot.cs @@ -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); diff --git a/SafeExamBrowser/SafeExamBrowser.csproj b/SafeExamBrowser/SafeExamBrowser.csproj index 380ae343..3ba34103 100644 --- a/SafeExamBrowser/SafeExamBrowser.csproj +++ b/SafeExamBrowser/SafeExamBrowser.csproj @@ -143,6 +143,10 @@ {e1be031a-4354-41e7-83e8-843ded4489ff} SafeExamBrowser.UserInterface + + {73724659-4150-4792-A94E-42F5F3C1B696} + SafeExamBrowser.WindowsApi +