251 lines
7.3 KiB
C#
251 lines
7.3 KiB
C#
/*
|
|
* Copyright (c) 2024 ETH Zürich, IT Services
|
|
*
|
|
* 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 System.Linq;
|
|
using System.Management;
|
|
using System.Threading.Tasks;
|
|
using System.Windows.Forms;
|
|
using Microsoft.Win32;
|
|
using SafeExamBrowser.Logging.Contracts;
|
|
using SafeExamBrowser.Monitoring.Contracts.Display;
|
|
using SafeExamBrowser.Monitoring.Contracts.Display.Events;
|
|
using SafeExamBrowser.Settings.Monitoring;
|
|
using SafeExamBrowser.SystemComponents.Contracts;
|
|
using SafeExamBrowser.WindowsApi.Contracts;
|
|
using OperatingSystem = SafeExamBrowser.SystemComponents.Contracts.OperatingSystem;
|
|
|
|
namespace SafeExamBrowser.Monitoring.Display
|
|
{
|
|
public class DisplayMonitor : IDisplayMonitor
|
|
{
|
|
private IBounds originalWorkingArea;
|
|
private readonly ILogger logger;
|
|
private readonly INativeMethods nativeMethods;
|
|
private readonly ISystemInfo systemInfo;
|
|
private string wallpaper;
|
|
|
|
public event DisplayChangedEventHandler DisplayChanged;
|
|
|
|
public DisplayMonitor(ILogger logger, INativeMethods nativeMethods, ISystemInfo systemInfo)
|
|
{
|
|
this.logger = logger;
|
|
this.nativeMethods = nativeMethods;
|
|
this.systemInfo = systemInfo;
|
|
}
|
|
|
|
public void InitializePrimaryDisplay(int taskbarHeight)
|
|
{
|
|
InitializeWorkingArea(taskbarHeight);
|
|
InitializeWallpaper();
|
|
}
|
|
|
|
public void ResetPrimaryDisplay()
|
|
{
|
|
ResetWorkingArea();
|
|
ResetWallpaper();
|
|
}
|
|
|
|
public void StartMonitoringDisplayChanges()
|
|
{
|
|
SystemEvents.DisplaySettingsChanged += SystemEvents_DisplaySettingsChanged;
|
|
logger.Info("Started monitoring display changes.");
|
|
}
|
|
|
|
public void StopMonitoringDisplayChanges()
|
|
{
|
|
SystemEvents.DisplaySettingsChanged -= SystemEvents_DisplaySettingsChanged;
|
|
logger.Info("Stopped monitoring display changes.");
|
|
}
|
|
|
|
public ValidationResult ValidateConfiguration(DisplaySettings settings)
|
|
{
|
|
var result = new ValidationResult();
|
|
|
|
if (TryLoadDisplays(out var displays))
|
|
{
|
|
var active = displays.Where(d => d.IsActive);
|
|
var count = active.Count();
|
|
|
|
result.ExternalDisplays = active.Count(d => !d.IsInternal);
|
|
result.InternalDisplays = active.Count(d => d.IsInternal);
|
|
result.IsAllowed = count <= settings.AllowedDisplays;
|
|
|
|
if (result.IsAllowed)
|
|
{
|
|
logger.Info($"Detected {count} active displays, {settings.AllowedDisplays} are allowed.");
|
|
}
|
|
else
|
|
{
|
|
logger.Warn($"Detected {count} active displays but only {settings.AllowedDisplays} are allowed!");
|
|
}
|
|
|
|
if (settings.InternalDisplayOnly && active.Any(d => !d.IsInternal))
|
|
{
|
|
result.IsAllowed = false;
|
|
logger.Warn("Detected external display but only internal displays are allowed!");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
result.IsAllowed = settings.IgnoreError;
|
|
logger.Warn($"Failed to validate display configuration, {(result.IsAllowed ? "ignoring error" : "active configuration is not allowed")}.");
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private void SystemEvents_DisplaySettingsChanged(object sender, EventArgs e)
|
|
{
|
|
logger.Info("Display change detected!");
|
|
Task.Run(() => DisplayChanged?.Invoke());
|
|
}
|
|
|
|
private void InitializeWorkingArea(int taskbarHeight)
|
|
{
|
|
var identifier = GetIdentifierForPrimaryDisplay();
|
|
|
|
if (originalWorkingArea == null)
|
|
{
|
|
originalWorkingArea = nativeMethods.GetWorkingArea();
|
|
LogWorkingArea($"Saved original working area for {identifier}", originalWorkingArea);
|
|
}
|
|
|
|
var area = new Bounds
|
|
{
|
|
Left = 0,
|
|
Top = 0,
|
|
Right = Screen.PrimaryScreen.Bounds.Width,
|
|
Bottom = Screen.PrimaryScreen.Bounds.Height - taskbarHeight
|
|
};
|
|
|
|
LogWorkingArea($"Trying to set new working area for {identifier}", area);
|
|
nativeMethods.SetWorkingArea(area);
|
|
LogWorkingArea($"Working area of {identifier} is now set to", nativeMethods.GetWorkingArea());
|
|
}
|
|
|
|
private void InitializeWallpaper()
|
|
{
|
|
if (systemInfo.OperatingSystem == OperatingSystem.Windows7)
|
|
{
|
|
var path = nativeMethods.GetWallpaperPath();
|
|
|
|
if (!string.IsNullOrEmpty(path))
|
|
{
|
|
wallpaper = path;
|
|
logger.Info($"Saved wallpaper image: {wallpaper}.");
|
|
}
|
|
|
|
nativeMethods.RemoveWallpaper();
|
|
logger.Info("Removed current wallpaper.");
|
|
}
|
|
}
|
|
|
|
private bool TryLoadDisplays(out IList<Display> displays)
|
|
{
|
|
var success = true;
|
|
|
|
displays = new List<Display>();
|
|
|
|
try
|
|
{
|
|
using (var searcher = new ManagementObjectSearcher(@"Root\WMI", "SELECT * FROM WmiMonitorBasicDisplayParams"))
|
|
using (var results = searcher.Get())
|
|
{
|
|
var displayParameters = results.Cast<ManagementObject>();
|
|
|
|
foreach (var display in displayParameters)
|
|
{
|
|
displays.Add(new Display
|
|
{
|
|
Identifier = Convert.ToString(display["InstanceName"]),
|
|
IsActive = Convert.ToBoolean(display["Active"])
|
|
});
|
|
}
|
|
}
|
|
|
|
using (var searcher = new ManagementObjectSearcher(@"Root\WMI", "SELECT * FROM WmiMonitorConnectionParams"))
|
|
using (var results = searcher.Get())
|
|
{
|
|
var connectionParameters = results.Cast<ManagementObject>();
|
|
|
|
foreach (var connection in connectionParameters)
|
|
{
|
|
var identifier = Convert.ToString(connection["InstanceName"]);
|
|
var isActive = Convert.ToBoolean(connection["Active"]);
|
|
var technologyValue = Convert.ToInt64(connection["VideoOutputTechnology"]);
|
|
var technology = (VideoOutputTechnology) technologyValue;
|
|
var display = displays.FirstOrDefault(d => d.Identifier?.Equals(identifier, StringComparison.OrdinalIgnoreCase) == true);
|
|
|
|
if (!Enum.IsDefined(typeof(VideoOutputTechnology), technology))
|
|
{
|
|
logger.Warn($"Detected undefined video output technology '{technologyValue}' for display '{identifier}'!");
|
|
}
|
|
|
|
if (display != default(Display))
|
|
{
|
|
display.IsActive &= isActive;
|
|
display.Technology = technology;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
success = false;
|
|
logger.Error("Failed to query displays!", e);
|
|
}
|
|
|
|
foreach (var display in displays)
|
|
{
|
|
logger.Info($"Detected {(display.IsActive ? "active" : "inactive")}, {(display.IsInternal ? "internal" : "external")} display '{display.Identifier}' connected via '{display.Technology}'.");
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
private void ResetWorkingArea()
|
|
{
|
|
var identifier = GetIdentifierForPrimaryDisplay();
|
|
|
|
if (originalWorkingArea != null)
|
|
{
|
|
nativeMethods.SetWorkingArea(originalWorkingArea);
|
|
LogWorkingArea($"Restored original working area for {identifier}", originalWorkingArea);
|
|
}
|
|
else
|
|
{
|
|
logger.Warn($"Could not restore original working area for {identifier}!");
|
|
}
|
|
}
|
|
|
|
private void ResetWallpaper()
|
|
{
|
|
if (systemInfo.OperatingSystem == OperatingSystem.Windows7 && !String.IsNullOrEmpty(wallpaper))
|
|
{
|
|
nativeMethods.SetWallpaper(wallpaper);
|
|
logger.Info($"Restored wallpaper image: {wallpaper}.");
|
|
}
|
|
}
|
|
|
|
private string GetIdentifierForPrimaryDisplay()
|
|
{
|
|
var name = Screen.PrimaryScreen.DeviceName?.Replace(@"\\.\", string.Empty);
|
|
var resolution = $"{Screen.PrimaryScreen.Bounds.Width}x{Screen.PrimaryScreen.Bounds.Height}";
|
|
var identifier = $"{name} ({resolution})";
|
|
|
|
return identifier;
|
|
}
|
|
|
|
private void LogWorkingArea(string message, IBounds area)
|
|
{
|
|
logger.Info($"{message}: Left = {area.Left}, Top = {area.Top}, Right = {area.Right}, Bottom = {area.Bottom}.");
|
|
}
|
|
}
|
|
}
|