SEBWIN-312: Implemented basic handling of whitelisted applications.

This commit is contained in:
dbuechel 2019-11-13 10:11:11 +01:00
parent 3d55bd6ff4
commit 35dd3dd4c2
19 changed files with 313 additions and 33 deletions

View file

@ -30,19 +30,29 @@ namespace SafeExamBrowser.Applications.Contracts
/// </summary>
event IconChangedEventHandler IconChanged;
/// <summary>
/// Event fired when the application instance has been terminated.
/// </summary>
event InstanceTerminatedEventHandler Terminated;
/// <summary>
/// Event fired when the name or (document) title of the application instance has changed.
/// </summary>
event NameChangedEventHandler NameChanged;
/// <summary>
/// Event fired when the application instance has been terminated.
/// </summary>
event InstanceTerminatedEventHandler Terminated;
/// <summary>
/// Makes this instance the currently active one and brings it to the foreground.
/// </summary>
void Activate();
/// <summary>
/// Initializes the application instance.
/// </summary>
void Initialize();
/// <summary>
/// Terminates the application instance.
/// </summary>
void Terminate();
}
}

View file

@ -14,16 +14,19 @@ using SafeExamBrowser.Applications.Contracts;
using SafeExamBrowser.Core.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Settings.Applications;
using SafeExamBrowser.WindowsApi.Contracts;
namespace SafeExamBrowser.Applications
{
public class ApplicationFactory : IApplicationFactory
{
private ILogger logger;
private IModuleLogger logger;
private IProcessFactory processFactory;
public ApplicationFactory(ILogger logger)
public ApplicationFactory(IModuleLogger logger, IProcessFactory processFactory)
{
this.logger = logger;
this.processFactory = processFactory;
}
public FactoryResult TryCreate(WhitelistApplication settings, out IApplication application)
@ -62,7 +65,7 @@ namespace SafeExamBrowser.Applications
{
var icon = new IconResource { Type = IconResourceType.Embedded, Uri = new Uri(executablePath) };
var info = new ApplicationInfo { IconResource = icon, Name = settings.DisplayName, Tooltip = settings.Description ?? settings.DisplayName };
var application = new ExternalApplication(executablePath, info);
var application = new ExternalApplication(executablePath, info, logger.CloneFor(settings.DisplayName), processFactory);
return application;
}

View file

@ -0,0 +1,42 @@
/*
* 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

@ -6,38 +6,64 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System.Collections.Generic;
using System.Linq;
using SafeExamBrowser.Applications.Contracts;
using SafeExamBrowser.Applications.Contracts.Events;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.WindowsApi.Contracts;
namespace SafeExamBrowser.Applications
{
internal class ExternalApplication : IApplication
{
private string executablePath;
private IModuleLogger logger;
private IList<IApplicationInstance> instances;
private IProcessFactory processFactory;
public ApplicationInfo Info { get; }
public event InstanceStartedEventHandler InstanceStarted;
internal ExternalApplication(string executablePath, ApplicationInfo info)
internal ExternalApplication(string executablePath, ApplicationInfo info, IModuleLogger logger, IProcessFactory processFactory)
{
this.executablePath = executablePath;
this.Info = info;
this.logger = logger;
this.instances = new List<IApplicationInstance>();
this.processFactory = processFactory;
}
public void Initialize()
{
// Nothing to do here for now.
}
public void Start()
{
logger.Info("Starting application...");
var process = processFactory.StartNew(executablePath);
var id = new ApplicationInstanceIdentifier(process.Id);
var instance = new ExternalApplicationInstance(id, logger.CloneFor($"{Info.Name} {id}"), process);
instance.Initialize();
instances.Add(instance);
InstanceStarted?.Invoke(instance);
}
public void Terminate()
{
if (instances.Any())
{
logger.Info("Terminating application...");
}
foreach (var instance in instances)
{
instance.Terminate();
}
}
}
}

View file

@ -0,0 +1,115 @@
/*
* 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 System.Timers;
using SafeExamBrowser.Applications.Contracts;
using SafeExamBrowser.Applications.Contracts.Events;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.WindowsApi.Contracts;
namespace SafeExamBrowser.Applications
{
internal class ExternalApplicationInstance : IApplicationInstance
{
private const int ONE_SECOND = 1000;
private ILogger logger;
private string name;
private IProcess process;
private Timer timer;
public InstanceIdentifier Id { get; }
public string Name { get; }
public event IconChangedEventHandler IconChanged { add { } remove { } }
public event NameChangedEventHandler NameChanged;
public event InstanceTerminatedEventHandler Terminated;
public ExternalApplicationInstance(InstanceIdentifier id, ILogger logger, IProcess process)
{
this.Id = id;
this.logger = logger;
this.process = process;
}
public void Activate()
{
var success = process.TryActivate();
if (!success)
{
logger.Warn("Failed to activate instance!");
}
}
public void Initialize()
{
process.Terminated += Process_Terminated;
timer = new Timer(ONE_SECOND);
timer.Elapsed += Timer_Elapsed;
timer.Start();
logger.Info("Initialized application instance.");
}
public void Terminate()
{
const int MAX_ATTEMPTS = 5;
const int TIMEOUT_MS = 500;
timer.Elapsed -= Timer_Elapsed;
timer?.Stop();
var terminated = process.HasTerminated;
for (var attempt = 0; attempt < MAX_ATTEMPTS && !terminated; attempt++)
{
terminated = process.TryClose(TIMEOUT_MS);
}
for (var attempt = 0; attempt < MAX_ATTEMPTS && !terminated; attempt++)
{
terminated = process.TryKill(TIMEOUT_MS);
}
if (terminated)
{
logger.Info("Successfully terminated application instance.");
}
else
{
logger.Warn("Failed to terminate application instance!");
}
}
private void Process_Terminated(int exitCode)
{
logger.Info($"Application instance has terminated with exit code {exitCode}.");
Terminated?.Invoke(Id);
}
private void Timer_Elapsed(object sender, ElapsedEventArgs e)
{
var success = process.TryGetWindowTitle(out var title);
var hasChanged = name?.Equals(title, StringComparison.Ordinal) != true;
// TODO: Use this and ensure log window doesn't hang on shutdown (if it is open)!
// logger.Warn($"Success: {success} HasChanged: {hasChanged}");
if (success && hasChanged)
{
name = title;
NameChanged?.Invoke(name);
}
timer.Start();
}
}
}

View file

@ -55,7 +55,9 @@
</ItemGroup>
<ItemGroup>
<Compile Include="ApplicationFactory.cs" />
<Compile Include="ApplicationInstanceIdentifier.cs" />
<Compile Include="ExternalApplication.cs" />
<Compile Include="ExternalApplicationInstance.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
@ -75,6 +77,10 @@
<Project>{30b2d907-5861-4f39-abad-c4abf1b3470e}</Project>
<Name>SafeExamBrowser.Settings</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.WindowsApi.Contracts\SafeExamBrowser.WindowsApi.Contracts.csproj">
<Project>{7016f080-9aa5-41b2-a225-385ad877c171}</Project>
<Name>SafeExamBrowser.WindowsApi.Contracts</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View file

@ -53,9 +53,9 @@ namespace SafeExamBrowser.Browser
public event DownloadRequestedEventHandler ConfigurationDownloadRequested;
public event IconChangedEventHandler IconChanged;
public event InstanceTerminatedEventHandler Terminated;
public event NameChangedEventHandler NameChanged;
public event PopupRequestedEventHandler PopupRequested;
public event InstanceTerminatedEventHandler Terminated;
public BrowserApplicationInstance(
AppConfig appConfig,
@ -85,13 +85,13 @@ namespace SafeExamBrowser.Browser
window?.BringToForeground();
}
internal void Initialize()
public void Initialize()
{
InitializeControl();
InitializeWindow();
}
internal void Terminate()
public void Terminate()
{
window?.Close();
}

View file

@ -12,7 +12,7 @@ namespace SafeExamBrowser.Browser
{
internal class BrowserInstanceIdentifier : InstanceIdentifier
{
public int Value { get; private set; }
internal int Value { get; private set; }
public BrowserInstanceIdentifier(int id)
{

View file

@ -96,7 +96,8 @@ namespace SafeExamBrowser.Client
taskbar = BuildTaskbar();
terminationActivator = new TerminationActivator(ModuleLogger(nameof(TerminationActivator)));
var applicationFactory = new ApplicationFactory(ModuleLogger(nameof(ApplicationFactory)));
var processFactory = new ProcessFactory(ModuleLogger(nameof(ProcessFactory)));
var applicationFactory = new ApplicationFactory(ModuleLogger(nameof(ApplicationFactory)), 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

@ -21,7 +21,7 @@
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" x:Name="ApplicationName" Background="#99D3D3D3" FontWeight="Bold" Padding="5" TextAlignment="Center" />
<ContentControl Grid.Row="1" x:Name="ApplicationButton" />
<ContentControl Grid.Row="1" x:Name="ApplicationButton" FontWeight="Bold" />
<StackPanel Grid.Row="2" x:Name="InstancePanel" Background="#99D3D3D3" Orientation="Vertical" />
</Grid>
</UserControl>

View file

@ -23,6 +23,6 @@
</ScrollViewer>
</Border>
</Popup>
<Button x:Name="Button" Background="{StaticResource BackgroundBrush}" Click="Button_Click" Padding="4" Template="{StaticResource TaskbarButton}" Width="50" />
<Button x:Name="Button" Background="{StaticResource BackgroundBrush}" Padding="4" Template="{StaticResource TaskbarButton}" Width="50" />
</Grid>
</UserControl>

View file

@ -21,7 +21,7 @@
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" x:Name="ApplicationName" Background="#99D3D3D3" FontWeight="Bold" Padding="5" TextAlignment="Center" />
<ContentControl Grid.Row="1" x:Name="ApplicationButton" />
<ContentControl Grid.Row="1" x:Name="ApplicationButton" FontWeight="Bold" />
<StackPanel Grid.Row="2" x:Name="InstancePanel" Background="#99D3D3D3" Orientation="Vertical" />
</Grid>
</UserControl>

View file

@ -23,6 +23,6 @@
</ScrollViewer>
</Border>
</Popup>
<Button x:Name="Button" Background="{StaticResource BackgroundBrush}" Click="Button_Click" Padding="4" Template="{StaticResource TaskbarButton}" Width="60" />
<Button x:Name="Button" Background="{StaticResource BackgroundBrush}" Padding="4" Template="{StaticResource TaskbarButton}" Width="60" />
</Grid>
</UserControl>

View file

@ -40,6 +40,12 @@ namespace SafeExamBrowser.WindowsApi.Contracts
/// </summary>
event ProcessTerminatedEventHandler Terminated;
/// <summary>
/// Attempts to activate the process (i.e. bring its main window to the foreground). This may only work for interactive processes which have
/// a main window. Returns <c>true</c> if the process was successfully activated, otherwise <c>false</c>.
/// </summary>
bool TryActivate();
/// <summary>
/// Attempts to gracefully terminate the process by closing its main window. This will only work for interactive processes which have a main
/// window. Optionally waits the specified amount of time for the process to terminate. Returns <c>true</c> if the process has terminated,
@ -47,6 +53,12 @@ namespace SafeExamBrowser.WindowsApi.Contracts
/// </summary>
bool TryClose(int timeout_ms = 0);
/// <summary>
/// Attempts to retrieve the title of the main window of the process. This will only work if for interactive processes which have a main
/// window. Returns <c>true</c> if the title was successfully retrieved, otherwise <c>false</c>.
/// </summary>
bool TryGetWindowTitle(out string title);
/// <summary>
/// Attempts to immediately kill the process. Optionally waits the specified amount of time for the process to terminate. Returns <c>true</c>
/// if the process has terminated, otherwise <c>false</c>.

View file

@ -19,7 +19,6 @@ namespace SafeExamBrowser.WindowsApi
internal class Kernel32
{
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool CloseHandle(IntPtr hObject);
[DllImport("kernel32.dll", SetLastError = true)]

View file

@ -11,9 +11,12 @@ 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
{
@ -62,6 +65,31 @@ namespace SafeExamBrowser.WindowsApi
this.originalNameInitialized = true;
}
public bool TryActivate()
{
try
{
var success = User32.BringWindowToTop(process.MainWindowHandle);
var placement = new WINDOWPLACEMENT();
placement.length = Marshal.SizeOf(placement);
User32.GetWindowPlacement(process.MainWindowHandle, ref placement);
if (placement.showCmd == (int) ShowWindowCommand.ShowMinimized)
{
success &= User32.ShowWindow(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
@ -90,6 +118,25 @@ 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

View file

@ -51,6 +51,7 @@
<ItemGroup>
<Reference Include="System" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Drawing" />
<Reference Include="System.Management" />
</ItemGroup>
<ItemGroup>
@ -91,6 +92,7 @@
<Compile Include="Types\RECT.cs" />
<Compile Include="Types\STARTUPINFO.cs" />
<Compile Include="Types\Window.cs" />
<Compile Include="Types\WINDOWPLACEMENT.cs" />
<Compile Include="User32.cs" />
</ItemGroup>
<ItemGroup>

View file

@ -0,0 +1,22 @@
/*
* 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.Drawing;
namespace SafeExamBrowser.WindowsApi.Types
{
internal struct WINDOWPLACEMENT
{
public int length;
public int flags;
public int showCmd;
public Point ptMinPosition;
public Point ptMaxPosition;
public Rectangle rcNormalPosition;
}
}

View file

@ -24,7 +24,6 @@ namespace SafeExamBrowser.WindowsApi
internal static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool CloseClipboard();
[DllImport("user32.dll", SetLastError = true)]
@ -34,15 +33,15 @@ namespace SafeExamBrowser.WindowsApi
internal static extern IntPtr CreateDesktop(string lpszDesktop, IntPtr lpszDevice, IntPtr pDevmode, int dwFlags, uint dwDesiredAccess, IntPtr lpsa);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool BringWindowToTop(IntPtr hWnd);
[DllImport("user32.dll", SetLastError = true)]
internal static extern bool EmptyClipboard();
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool EnumDesktops(IntPtr hwinsta, EnumDesktopDelegate lpEnumFunc, IntPtr lParam);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool EnumWindows(EnumWindowsDelegate enumProc, IntPtr lParam);
[DllImport("user32.dll", SetLastError = true)]
@ -60,6 +59,9 @@ namespace SafeExamBrowser.WindowsApi
[DllImport("user32.dll", SetLastError = true)]
internal static extern bool GetUserObjectInformation(IntPtr hObj, int nIndex, IntPtr pvInfo, int nLength, ref int lpnLengthNeeded);
[DllImport("user32.dll", SetLastError = true)]
internal static extern bool GetWindowPlacement(IntPtr hWnd, ref WINDOWPLACEMENT lpwndpl);
[DllImport("user32.dll", SetLastError = true)]
internal static extern int GetWindowText(IntPtr hWnd, StringBuilder strText, int maxCount);
@ -70,15 +72,12 @@ namespace SafeExamBrowser.WindowsApi
internal static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool IsWindowVisible(IntPtr hWnd);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool OpenClipboard(IntPtr hWndNewOwner);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool PostMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", SetLastError = true)]
@ -91,25 +90,21 @@ namespace SafeExamBrowser.WindowsApi
internal static extern IntPtr SetWindowsHookEx(HookType hookType, HookDelegate lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll", SetLastError = true)]
internal static extern bool SwitchDesktop(IntPtr hDesktop);
[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)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool SystemParametersInfo(SPI uiAction, int uiParam, string pvParam, SPIF fWinIni);
[DllImport("user32.dll", SetLastError = true)]
internal static extern bool UnhookWinEvent(IntPtr hWinEventHook);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool UnhookWindowsHookEx(IntPtr hhk);
}
}