SEBWIN-312: Implemented basic window handling for external applications. Reverted wrong cleanup logic for native handles.

This commit is contained in:
dbuechel 2019-11-29 14:59:54 +01:00
parent 4503cbe778
commit f19f284d95
20 changed files with 243 additions and 221 deletions

View file

@ -1,41 +0,0 @@
/*
* Copyright (c) 2019 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/.
*/
namespace SafeExamBrowser.Applications.Contracts
{
/// <summary>
/// Defines an identifier which uniquely identifies an instance in the context of an application.
/// </summary>
public abstract class InstanceIdentifier
{
/// <summary>
/// Determines whether two identifiers are equal (i.e. whether they identify the same application instance).
/// </summary>
public static bool operator ==(InstanceIdentifier a, InstanceIdentifier b) => Equals(a, b);
/// <summary>
/// Determines whether two identifiers are different (i.e. whether they identify different application instances).
/// </summary>
public static bool operator !=(InstanceIdentifier a, InstanceIdentifier b) => !Equals(a, b);
/// <summary>
/// Indicates whether the given object is an identifier for the same application instance.
/// </summary>
public abstract override bool Equals(object other);
/// <summary>
/// Returns a hash code for the identifier.
/// </summary>
public abstract override int GetHashCode();
/// <summary>
/// Returns a human-readable string representation of the identifier.
/// </summary>
public abstract override string ToString();
}
}

View file

@ -62,7 +62,6 @@
<Compile Include="IApplicationFactory.cs" />
<Compile Include="ApplicationInfo.cs" />
<Compile Include="IApplicationWindow.cs" />
<Compile Include="InstanceIdentifier.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>

View file

@ -21,11 +21,13 @@ namespace SafeExamBrowser.Applications
public class ApplicationFactory : IApplicationFactory
{
private IModuleLogger logger;
private INativeMethods nativeMethods;
private IProcessFactory processFactory;
public ApplicationFactory(IModuleLogger logger, IProcessFactory processFactory)
public ApplicationFactory(IModuleLogger logger, INativeMethods nativeMethods, IProcessFactory processFactory)
{
this.logger = logger;
this.nativeMethods = nativeMethods;
this.processFactory = processFactory;
}
@ -65,7 +67,7 @@ namespace SafeExamBrowser.Applications
{
var icon = new IconResource { Type = IconResourceType.Embedded, Uri = new Uri(executablePath) };
var info = new ApplicationInfo { AutoStart = settings.AutoStart, Icon = icon, Name = settings.DisplayName, Tooltip = settings.Description ?? settings.DisplayName };
var application = new ExternalApplication(executablePath, info, logger.CloneFor(settings.DisplayName), processFactory);
var application = new ExternalApplication(executablePath, info, logger.CloneFor(settings.DisplayName), nativeMethods, processFactory);
return application;
}

View file

@ -1,42 +0,0 @@
/*
* Copyright (c) 2019 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 SafeExamBrowser.Applications.Contracts;
namespace SafeExamBrowser.Applications
{
internal class ApplicationInstanceIdentifier : InstanceIdentifier
{
internal int ProcessId { get; private set; }
public ApplicationInstanceIdentifier(int processId)
{
ProcessId = processId;
}
public override bool Equals(object other)
{
if (other is ApplicationInstanceIdentifier id)
{
return ProcessId == id.ProcessId;
}
return false;
}
public override int GetHashCode()
{
return ProcessId.GetHashCode();
}
public override string ToString()
{
return $"({ProcessId})";
}
}
}

View file

@ -0,0 +1,12 @@
/*
* Copyright (c) 2019 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/.
*/
namespace SafeExamBrowser.Applications.Events
{
internal delegate void InstanceTerminatedEventHandler(int id);
}

View file

@ -18,8 +18,11 @@ namespace SafeExamBrowser.Applications
{
internal class ExternalApplication : IApplication
{
private int instanceIdCounter = default(int);
private string executablePath;
private IModuleLogger logger;
private INativeMethods nativeMethods;
private IList<ExternalApplicationInstance> instances;
private IProcessFactory processFactory;
@ -27,18 +30,24 @@ namespace SafeExamBrowser.Applications
public ApplicationInfo Info { get; }
internal ExternalApplication(string executablePath, ApplicationInfo info, IModuleLogger logger, IProcessFactory processFactory)
internal ExternalApplication(
string executablePath,
ApplicationInfo info,
IModuleLogger logger,
INativeMethods nativeMethods,
IProcessFactory processFactory)
{
this.executablePath = executablePath;
this.Info = info;
this.logger = logger;
this.nativeMethods = nativeMethods;
this.instances = new List<ExternalApplicationInstance>();
this.processFactory = processFactory;
}
public IEnumerable<IApplicationWindow> GetWindows()
{
return Enumerable.Empty<IApplicationWindow>();
return instances.SelectMany(i => i.GetWindows());
}
public void Initialize()
@ -53,10 +62,13 @@ namespace SafeExamBrowser.Applications
logger.Info("Starting application...");
var process = processFactory.StartNew(executablePath);
var id = new ApplicationInstanceIdentifier(process.Id);
var instance = new ExternalApplicationInstance(Info.Icon, id, logger.CloneFor($"{Info.Name} {id}"), process);
var id = ++instanceIdCounter;
var instanceLogger = logger.CloneFor($"{Info.Name} Instance #{id}");
var instance = new ExternalApplicationInstance(Info.Icon, id, instanceLogger, nativeMethods, process);
instance.Initialize();
instance.Terminated += Instance_Terminated;
instance.WindowsChanged += () => WindowsChanged?.Invoke();
instances.Add(instance);
}
catch (Exception e)
@ -65,6 +77,12 @@ namespace SafeExamBrowser.Applications
}
}
private void Instance_Terminated(int id)
{
instances.Remove(instances.First(i => i.Id == id));
WindowsChanged?.Invoke();
}
public void Terminate()
{
if (instances.Any())

View file

@ -6,7 +6,13 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Timers;
using SafeExamBrowser.Applications.Contracts;
using SafeExamBrowser.Applications.Contracts.Events;
using SafeExamBrowser.Applications.Events;
using SafeExamBrowser.Core.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.WindowsApi.Contracts;
@ -15,22 +21,42 @@ namespace SafeExamBrowser.Applications
{
internal class ExternalApplicationInstance
{
private IconResource icon;
private InstanceIdentifier id;
private ILogger logger;
private IProcess process;
private readonly object @lock = new object();
internal ExternalApplicationInstance(IconResource icon, InstanceIdentifier id, ILogger logger, IProcess process)
private IconResource icon;
private ILogger logger;
private INativeMethods nativeMethods;
private IProcess process;
private Timer timer;
private IList<ExternalApplicationWindow> windows;
internal int Id { get; }
internal event InstanceTerminatedEventHandler Terminated;
internal event WindowsChangedEventHandler WindowsChanged;
internal ExternalApplicationInstance(IconResource icon, int id, ILogger logger, INativeMethods nativeMethods, IProcess process)
{
this.icon = icon;
this.id = id;
this.Id = id;
this.logger = logger;
this.nativeMethods = nativeMethods;
this.process = process;
this.windows = new List<ExternalApplicationWindow>();
}
internal IEnumerable<IApplicationWindow> GetWindows()
{
lock (@lock)
{
return new List<IApplicationWindow>(windows);
}
}
internal void Initialize()
{
process.Terminated += Process_Terminated;
InitializeEvents();
logger.Info("Initialized application instance.");
}
internal void Terminate()
@ -42,7 +68,7 @@ namespace SafeExamBrowser.Applications
if (!terminated)
{
process.Terminated -= Process_Terminated;
FinalizeEvents();
for (var attempt = 0; attempt < MAX_ATTEMPTS && !terminated; attempt++)
{
@ -68,7 +94,69 @@ namespace SafeExamBrowser.Applications
private void Process_Terminated(int exitCode)
{
logger.Info($"Application instance has terminated with exit code {exitCode}.");
// TODO: Terminated?.Invoke(Id); -> Remove from application!
FinalizeEvents();
Terminated?.Invoke(Id);
}
private void Timer_Elapsed(object sender, ElapsedEventArgs e)
{
var changed = false;
var openWindows = nativeMethods.GetOpenWindows();
lock (@lock)
{
var closedWindows = windows.Where(w => openWindows.All(ow => ow != w.Handle)).ToList();
var openedWindows = openWindows.Where(ow => windows.All(w => w.Handle != ow) && BelongsToInstance(ow));
foreach (var window in closedWindows)
{
changed = true;
windows.Remove(window);
}
foreach (var window in openedWindows)
{
changed = true;
windows.Add(new ExternalApplicationWindow(icon, nativeMethods, window));
}
foreach (var window in windows)
{
window.Update();
}
}
if (changed)
{
logger.Error("WINDOWS CHANGED!");
WindowsChanged?.Invoke();
}
timer.Start();
}
private bool BelongsToInstance(IntPtr window)
{
return nativeMethods.GetProcessIdFor(window) == process.Id;
}
private void InitializeEvents()
{
const int ONE_SECOND = 1000;
process.Terminated += Process_Terminated;
timer = new Timer(ONE_SECOND);
timer.Elapsed += Timer_Elapsed;
timer.Start();
}
private void FinalizeEvents()
{
timer.Elapsed -= Timer_Elapsed;
timer.Stop();
process.Terminated -= Process_Terminated;
}
}
}

View file

@ -0,0 +1,52 @@
/*
* Copyright (c) 2019 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 SafeExamBrowser.Applications.Contracts;
using SafeExamBrowser.Applications.Contracts.Events;
using SafeExamBrowser.Core.Contracts;
using SafeExamBrowser.WindowsApi.Contracts;
namespace SafeExamBrowser.Applications
{
internal class ExternalApplicationWindow : IApplicationWindow
{
private INativeMethods nativeMethods;
internal IntPtr Handle { get; }
public IconResource Icon { get; }
public string Title { get; private set; }
public event IconChangedEventHandler IconChanged { add { } remove { } }
public event TitleChangedEventHandler TitleChanged;
internal ExternalApplicationWindow(IconResource icon, INativeMethods nativeMethods, IntPtr handle)
{
this.Handle = handle;
this.Icon = icon;
this.nativeMethods = nativeMethods;
}
public void Activate()
{
nativeMethods.ActivateWindow(Handle);
}
internal void Update()
{
var title = nativeMethods.GetWindowTitle(Handle);
var hasChanged = Title?.Equals(title, StringComparison.Ordinal) != true;
if (hasChanged)
{
Title = title;
TitleChanged?.Invoke(title);
}
}
}
}

View file

@ -55,9 +55,10 @@
</ItemGroup>
<ItemGroup>
<Compile Include="ApplicationFactory.cs" />
<Compile Include="ApplicationInstanceIdentifier.cs" />
<Compile Include="Events\InstanceTerminatedEventHandler.cs" />
<Compile Include="ExternalApplication.cs" />
<Compile Include="ExternalApplicationInstance.cs" />
<Compile Include="ExternalApplicationWindow.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>

View file

@ -112,9 +112,9 @@ namespace SafeExamBrowser.Browser
private void CreateNewInstance(string url = null)
{
var id = new BrowserInstanceIdentifier(++instanceIdCounter);
var id = ++instanceIdCounter;
var isMainInstance = instances.Count == 0;
var instanceLogger = logger.CloneFor($"BrowserInstance {id}");
var instanceLogger = logger.CloneFor($"Browser Instance #{id}");
var startUrl = url ?? settings.StartUrl;
var instance = new BrowserApplicationInstance(appConfig, settings, id, isMainInstance, messageBox, instanceLogger, text, uiFactory, startUrl);
@ -157,10 +157,9 @@ namespace SafeExamBrowser.Browser
CreateNewInstance(args.Url);
}
private void Instance_Terminated(InstanceIdentifier id)
private void Instance_Terminated(int id)
{
instances.Remove(instances.FirstOrDefault(i => i.Id == id));
logger.Info($"Browser instance {id} was terminated.");
instances.Remove(instances.First(i => i.Id == id));
WindowsChanged?.Invoke();
}

View file

@ -49,7 +49,7 @@ namespace SafeExamBrowser.Browser
get { return isMainInstance ? settings.MainWindow : settings.AdditionalWindow; }
}
internal BrowserInstanceIdentifier Id { get; private set; }
internal int Id { get; }
public IconResource Icon { get; private set; }
public string Title { get; private set; }
@ -64,7 +64,7 @@ namespace SafeExamBrowser.Browser
public BrowserApplicationInstance(
AppConfig appConfig,
BrowserSettings settings,
BrowserInstanceIdentifier id,
int id,
bool isMainInstance,
IMessageBox messageBox,
IModuleLogger logger,
@ -166,7 +166,7 @@ namespace SafeExamBrowser.Browser
private void InitializeWindow()
{
window = uiFactory.CreateBrowserWindow(control, settings, isMainInstance);
window.Closing += () => Terminated?.Invoke(Id);
window.Closing += Window_Closing;
window.AddressChanged += Window_AddressChanged;
window.BackwardNavigationRequested += Window_BackwardNavigationRequested;
window.DeveloperConsoleRequested += Window_DeveloperConsoleRequested;
@ -326,6 +326,12 @@ namespace SafeExamBrowser.Browser
control.NavigateBackwards();
}
private void Window_Closing()
{
logger.Info($"Instance has terminated.");
Terminated?.Invoke(Id);
}
private void Window_DeveloperConsoleRequested()
{
logger.Debug("Showing developer console...");

View file

@ -1,42 +0,0 @@
/*
* Copyright (c) 2019 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 SafeExamBrowser.Applications.Contracts;
namespace SafeExamBrowser.Browser
{
internal class BrowserInstanceIdentifier : InstanceIdentifier
{
internal int Value { get; private set; }
public BrowserInstanceIdentifier(int id)
{
Value = id;
}
public override bool Equals(object other)
{
if (other is BrowserInstanceIdentifier id)
{
return Value == id.Value;
}
return false;
}
public override int GetHashCode()
{
return Value.GetHashCode();
}
public override string ToString()
{
return $"#{Value}";
}
}
}

View file

@ -8,5 +8,5 @@
namespace SafeExamBrowser.Browser.Events
{
internal delegate void InstanceTerminatedEventHandler(BrowserInstanceIdentifier id);
internal delegate void InstanceTerminatedEventHandler(int id);
}

View file

@ -63,7 +63,6 @@
<ItemGroup>
<Compile Include="BrowserApplication.cs" />
<Compile Include="BrowserApplicationInstance.cs" />
<Compile Include="BrowserInstanceIdentifier.cs" />
<Compile Include="Events\FaviconChangedEventHandler.cs" />
<Compile Include="Events\InstanceTerminatedEventHandler.cs" />
<Compile Include="Events\PopupRequestedEventArgs.cs" />

View file

@ -98,7 +98,7 @@ namespace SafeExamBrowser.Client
taskView = BuildTaskView();
var processFactory = new ProcessFactory(ModuleLogger(nameof(ProcessFactory)));
var applicationFactory = new ApplicationFactory(ModuleLogger(nameof(ApplicationFactory)), processFactory);
var applicationFactory = new ApplicationFactory(ModuleLogger(nameof(ApplicationFactory)), nativeMethods, processFactory);
var applicationMonitor = new ApplicationMonitor(TWO_SECONDS, ModuleLogger(nameof(ApplicationMonitor)), nativeMethods, new ProcessFactory(ModuleLogger(nameof(ProcessFactory))));
var displayMonitor = new DisplayMonitor(ModuleLogger(nameof(DisplayMonitor)), nativeMethods, systemInfo);
var explorerShell = new ExplorerShell(ModuleLogger(nameof(ExplorerShell)), nativeMethods);

View file

@ -145,7 +145,7 @@ namespace SafeExamBrowser.UserInterface.Desktop
Left = (SystemParameters.WorkArea.Width - Width) / 2 + SystemParameters.WorkArea.Left;
Top = (SystemParameters.WorkArea.Height - Height) / 2 + SystemParameters.WorkArea.Top;
if (!windows.Any())
if (!controls.Any())
{
Hide();
}

View file

@ -145,7 +145,7 @@ namespace SafeExamBrowser.UserInterface.Mobile
Left = (SystemParameters.WorkArea.Width - Width) / 2 + SystemParameters.WorkArea.Left;
Top = (SystemParameters.WorkArea.Height - Height) / 2 + SystemParameters.WorkArea.Top;
if (!windows.Any())
if (!controls.Any())
{
Hide();
}

View file

@ -17,6 +17,11 @@ namespace SafeExamBrowser.WindowsApi.Contracts
/// </summary>
public interface INativeMethods
{
/// <summary>
/// Brings the window with the given handle to the foreground and activates it.
/// </summary>
void ActivateWindow(IntPtr handle);
/// <summary>
/// Deregisters a previously registered keyboard hook.
/// </summary>
@ -86,7 +91,7 @@ namespace SafeExamBrowser.WindowsApi.Contracts
string GetWallpaperPath();
/// <summary>
/// Retrieves the title of the specified window, or an empty string, if the given window does not have a title.
/// Retrieves the title of the window with the given handle, or an empty string if the given window does not have a title.
/// </summary>
string GetWindowTitle(IntPtr window);

View file

@ -24,34 +24,50 @@ namespace SafeExamBrowser.WindowsApi
{
public class NativeMethods : INativeMethods
{
private ConcurrentBag<KeyboardHook> KeyboardHooks = new ConcurrentBag<KeyboardHook>();
private ConcurrentBag<MouseHook> MouseHooks = new ConcurrentBag<MouseHook>();
private ConcurrentBag<SystemHook> SystemHooks = new ConcurrentBag<SystemHook>();
private ConcurrentDictionary<Guid, KeyboardHook> KeyboardHooks = new ConcurrentDictionary<Guid, KeyboardHook>();
private ConcurrentDictionary<Guid, MouseHook> MouseHooks = new ConcurrentDictionary<Guid, MouseHook>();
private ConcurrentDictionary<Guid, SystemHook> SystemHooks = new ConcurrentDictionary<Guid, SystemHook>();
/// <summary>
/// Upon finalization, unregister all active system events and hooks...
/// </summary>
~NativeMethods()
{
foreach (var hook in SystemHooks)
foreach (var hook in SystemHooks.Values)
{
hook.Detach();
}
foreach (var hook in KeyboardHooks)
foreach (var hook in KeyboardHooks.Values)
{
hook.Detach();
}
foreach (var hook in MouseHooks)
foreach (var hook in MouseHooks.Values)
{
hook.Detach();
}
}
public void ActivateWindow(IntPtr handle)
{
var placement = new WINDOWPLACEMENT();
User32.BringWindowToTop(handle);
User32.SetForegroundWindow(handle);
placement.length = Marshal.SizeOf(placement);
User32.GetWindowPlacement(handle, ref placement);
if (placement.showCmd == (int) ShowWindowCommand.ShowMinimized)
{
User32.ShowWindowAsync(handle, (int) ShowWindowCommand.Restore);
}
}
public void DeregisterKeyboardHook(Guid hookId)
{
var hook = KeyboardHooks.FirstOrDefault(h => h.Id == hookId);
var hook = KeyboardHooks.Values.FirstOrDefault(h => h.Id == hookId);
if (hook != null)
{
@ -62,13 +78,13 @@ namespace SafeExamBrowser.WindowsApi
throw new Win32Exception(Marshal.GetLastWin32Error());
}
KeyboardHooks.TryTake(out _);
KeyboardHooks.TryRemove(hookId, out _);
}
}
public void DeregisterMouseHook(Guid hookId)
{
var hook = MouseHooks.FirstOrDefault(h => h.Id == hookId);
var hook = MouseHooks.Values.FirstOrDefault(h => h.Id == hookId);
if (hook != null)
{
@ -79,13 +95,13 @@ namespace SafeExamBrowser.WindowsApi
throw new Win32Exception(Marshal.GetLastWin32Error());
}
MouseHooks.TryTake(out _);
MouseHooks.TryRemove(hookId, out _);
}
}
public void DeregisterSystemEventHook(Guid hookId)
{
var hook = SystemHooks.FirstOrDefault(h => h.Id == hookId);
var hook = SystemHooks.Values.FirstOrDefault(h => h.Id == hookId);
if (hook != null)
{
@ -96,7 +112,7 @@ namespace SafeExamBrowser.WindowsApi
throw new Win32Exception(Marshal.GetLastWin32Error());
}
SystemHooks.TryTake(out _);
SystemHooks.TryRemove(hookId, out _);
}
}
@ -254,7 +270,7 @@ namespace SafeExamBrowser.WindowsApi
hook.Attach();
hookId = hook.Id;
KeyboardHooks.Add(hook);
KeyboardHooks[hookId] = hook;
hookReadyEvent.Set();
while (true)
@ -283,7 +299,7 @@ namespace SafeExamBrowser.WindowsApi
hook.Attach();
hookId = hook.Id;
MouseHooks.Add(hook);
MouseHooks[hookId] = hook;
hookReadyEvent.Set();
while (true)
@ -321,7 +337,7 @@ namespace SafeExamBrowser.WindowsApi
hook.Attach();
hookId = hook.Id;
SystemHooks.Add(hook);
SystemHooks[hookId] = hook;
hookReadyEvent.Set();
hook.AwaitDetach();
});

View file

@ -11,12 +11,9 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Management;
using System.Runtime.InteropServices;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.WindowsApi.Constants;
using SafeExamBrowser.WindowsApi.Contracts;
using SafeExamBrowser.WindowsApi.Contracts.Events;
using SafeExamBrowser.WindowsApi.Types;
namespace SafeExamBrowser.WindowsApi
{
@ -68,34 +65,6 @@ namespace SafeExamBrowser.WindowsApi
this.namesInitialized = true;
}
public bool TryActivate()
{
try
{
var success = true;
var placement = new WINDOWPLACEMENT();
success &= User32.BringWindowToTop(process.MainWindowHandle);
success &= User32.SetForegroundWindow(process.MainWindowHandle);
placement.length = Marshal.SizeOf(placement);
User32.GetWindowPlacement(process.MainWindowHandle, ref placement);
if (placement.showCmd == (int) ShowWindowCommand.ShowMinimized)
{
success &= User32.ShowWindowAsync(process.MainWindowHandle, (int) ShowWindowCommand.Restore);
}
return success;
}
catch (Exception e)
{
logger.Error("Failed to activate process!", e);
}
return false;
}
public bool TryClose(int timeout_ms = 0)
{
try
@ -124,25 +93,6 @@ namespace SafeExamBrowser.WindowsApi
return false;
}
public bool TryGetWindowTitle(out string title)
{
title = default(string);
try
{
process.Refresh();
title = process.MainWindowTitle;
return true;
}
catch (Exception e)
{
logger.Error("Failed to retrieve title of main window!", e);
}
return false;
}
public bool TryKill(int timeout_ms = 0)
{
try