Restore SEBPatch

This commit is contained in:
2025-06-01 11:44:20 +02:00
commit 8c656e3137
1297 changed files with 142172 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8"/>
</startup>
</configuration>

View File

@@ -0,0 +1,79 @@
/*
* 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 SafeExamBrowser.Communication.Hosts;
using SafeExamBrowser.Communication.Contracts;
using SafeExamBrowser.Communication.Contracts.Data;
using SafeExamBrowser.Communication.Contracts.Events;
using SafeExamBrowser.Communication.Contracts.Hosts;
using SafeExamBrowser.Logging.Contracts;
namespace SafeExamBrowser.Service.Communication
{
internal class ServiceHost : BaseHost, IServiceHost
{
private bool allowConnection;
public event CommunicationEventHandler<SessionStartEventArgs> SessionStartRequested;
public event CommunicationEventHandler<SessionStopEventArgs> SessionStopRequested;
public event CommunicationEventHandler SystemConfigurationUpdateRequested;
internal ServiceHost(string address, IHostObjectFactory factory, ILogger logger, int timeout_ms) : base(address, factory, logger, timeout_ms)
{
allowConnection = true;
}
protected override bool OnConnect(Guid? token)
{
var allow = allowConnection;
if (allow)
{
allowConnection = false;
}
return allow;
}
protected override void OnDisconnect(Interlocutor interlocutor)
{
if (interlocutor == Interlocutor.Runtime)
{
allowConnection = true;
}
}
protected override Response OnReceive(Message message)
{
switch (message)
{
case SessionStartMessage m:
SessionStartRequested?.InvokeAsync(new SessionStartEventArgs { Configuration = m.Configuration });
return new SimpleResponse(SimpleResponsePurport.Acknowledged);
case SessionStopMessage m:
SessionStopRequested?.InvokeAsync(new SessionStopEventArgs { SessionId = m.SessionId });
return new SimpleResponse(SimpleResponsePurport.Acknowledged);
}
return new SimpleResponse(SimpleResponsePurport.UnknownMessage);
}
protected override Response OnReceive(SimpleMessagePurport message)
{
switch (message)
{
case SimpleMessagePurport.UpdateSystemConfiguration:
SystemConfigurationUpdateRequested?.InvokeAsync();
return new SimpleResponse(SimpleResponsePurport.Acknowledged);
}
return new SimpleResponse(SimpleResponsePurport.UnknownMessage);
}
}
}

View File

@@ -0,0 +1,126 @@
/*
* 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.IO;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Threading;
using SafeExamBrowser.Communication.Hosts;
using SafeExamBrowser.Communication.Proxies;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.OperationModel;
using SafeExamBrowser.Core.Operations;
using SafeExamBrowser.Lockdown;
using SafeExamBrowser.Logging;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Service.Communication;
using SafeExamBrowser.Service.Operations;
using SafeExamBrowser.Settings.Logging;
namespace SafeExamBrowser.Service
{
internal class CompositionRoot
{
private ILogger logger;
internal ServiceController ServiceController { get; private set; }
internal void BuildObjectGraph()
{
const int ONE_SECOND = 1000;
const int FIVE_SECONDS = 5000;
const int FIFTEEN_SECONDS = 15000;
var backupFilePath = BuildBackupFilePath();
InitializeLogging();
var featureBackup = new FeatureConfigurationBackup(backupFilePath, new ModuleLogger(logger, nameof(FeatureConfigurationBackup)));
var featureFactory = new FeatureConfigurationFactory(new ModuleLogger(logger, nameof(FeatureConfigurationFactory)));
var featureMonitor = new FeatureConfigurationMonitor(new ModuleLogger(logger, nameof(FeatureConfigurationMonitor)), ONE_SECOND);
var proxyFactory = new ProxyFactory(new ProxyObjectFactory(), new ModuleLogger(logger, nameof(ProxyFactory)));
var serviceHost = new ServiceHost(AppConfig.SERVICE_ADDRESS, new HostObjectFactory(), new ModuleLogger(logger, nameof(ServiceHost)), FIFTEEN_SECONDS);
var sessionContext = new SessionContext();
var systemConfigurationUpdate = new SystemConfigurationUpdate(new ModuleLogger(logger, nameof(SystemConfigurationUpdate)));
var bootstrapOperations = new Queue<IOperation>();
var sessionOperations = new Queue<IOperation>();
sessionContext.AutoRestoreMechanism = new AutoRestoreMechanism(featureBackup, new ModuleLogger(logger, nameof(AutoRestoreMechanism)), systemConfigurationUpdate, FIVE_SECONDS);
bootstrapOperations.Enqueue(new RestoreOperation(featureBackup, logger, sessionContext));
bootstrapOperations.Enqueue(new CommunicationHostOperation(serviceHost, logger));
bootstrapOperations.Enqueue(new ServiceEventCleanupOperation(logger, sessionContext));
sessionOperations.Enqueue(new SessionInitializationOperation(logger, ServiceEventFactory, sessionContext));
sessionOperations.Enqueue(new LockdownOperation(featureBackup, featureFactory, featureMonitor, logger, sessionContext));
sessionOperations.Enqueue(new SessionActivationOperation(logger, sessionContext));
var bootstrapSequence = new OperationSequence(logger, bootstrapOperations);
var sessionSequence = new OperationSequence(logger, sessionOperations);
ServiceController = new ServiceController(logger, LogWriterFactory, bootstrapSequence, sessionSequence, serviceHost, sessionContext, systemConfigurationUpdate);
}
private string BuildBackupFilePath()
{
var appDataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), nameof(SafeExamBrowser));
var filePath = Path.Combine(appDataFolder, AppConfig.BACKUP_FILE_NAME);
return filePath;
}
internal void LogStartupInformation()
{
logger.Log($"# Service started at {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
logger.Log(string.Empty);
}
internal void LogShutdownInformation()
{
logger?.Log($"# Service terminated at {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
}
private void InitializeLogging()
{
var appDataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), nameof(SafeExamBrowser));
var logFolder = Path.Combine(appDataFolder, "Logs");
var logFilePrefix = DateTime.Now.ToString("yyyy-MM-dd\\_HH\\hmm\\mss\\s");
var logFilePath = Path.Combine(logFolder, $"{logFilePrefix}_Service.log");
var logFileWriter = new LogFileWriter(new DefaultLogFormatter(), logFilePath);
logger = new Logger();
logger.LogLevel = LogLevel.Debug;
logger.Subscribe(logFileWriter);
logFileWriter.Initialize();
}
private ILogObserver LogWriterFactory(string filePath)
{
var writer = new LogFileWriter(new DefaultLogFormatter(), filePath);
writer.Initialize();
return writer;
}
private EventWaitHandle ServiceEventFactory(string eventName)
{
var securityIdentifier = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null);
var accessRule = new EventWaitHandleAccessRule(securityIdentifier, EventWaitHandleRights.Synchronize, AccessControlType.Allow);
var security = new EventWaitHandleSecurity();
security.AddAccessRule(accessRule);
return new EventWaitHandle(false, EventResetMode.AutoReset, eventName, out _, security);
}
}
}

View File

@@ -0,0 +1,35 @@
/*
* 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.ComponentModel;
using System.ServiceProcess;
namespace SafeExamBrowser.Service
{
[RunInstaller(true)]
public class Installer : System.Configuration.Install.Installer
{
private ServiceProcessInstaller process;
private ServiceInstaller service;
public Installer()
{
process = new ServiceProcessInstaller();
process.Account = ServiceAccount.LocalSystem;
service = new ServiceInstaller();
service.Description = "Performs operations which require elevated privileges.";
service.DisplayName = "Safe Exam Browser Service";
service.ServiceName = nameof(SafeExamBrowser);
service.StartType = ServiceStartMode.Automatic;
Installers.Add(process);
Installers.Add(service);
}
}
}

View File

@@ -0,0 +1,166 @@
/*
* 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 SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Lockdown.Contracts;
using SafeExamBrowser.Logging.Contracts;
namespace SafeExamBrowser.Service.Operations
{
internal class LockdownOperation : SessionOperation
{
private readonly IFeatureConfigurationBackup backup;
private readonly IFeatureConfigurationFactory factory;
private readonly IFeatureConfigurationMonitor monitor;
private readonly ILogger logger;
private Guid groupId;
public LockdownOperation(
IFeatureConfigurationBackup backup,
IFeatureConfigurationFactory factory,
IFeatureConfigurationMonitor monitor,
ILogger logger,
SessionContext sessionContext) : base(sessionContext)
{
this.backup = backup;
this.factory = factory;
this.monitor = monitor;
this.logger = logger;
}
public override OperationResult Perform()
{
groupId = Guid.NewGuid();
var success = true;
var sid = Context.Configuration.UserSid;
var userName = Context.Configuration.UserName;
var configurations = new List<(IFeatureConfiguration, bool)>
{
(factory.CreateChangePasswordConfiguration(groupId, sid, userName), Context.Configuration.Settings.Service.DisablePasswordChange),
(factory.CreateChromeNotificationConfiguration(groupId, sid, userName), Context.Configuration.Settings.Service.DisableChromeNotifications),
(factory.CreateEaseOfAccessConfiguration(groupId), Context.Configuration.Settings.Service.DisableEaseOfAccessOptions),
(factory.CreateFindPrinterConfiguration(groupId, sid, userName), Context.Configuration.Settings.Service.DisableFindPrinter),
(factory.CreateLockWorkstationConfiguration(groupId, sid, userName), Context.Configuration.Settings.Service.DisableUserLock),
(factory.CreateMachinePowerOptionsConfiguration(groupId), Context.Configuration.Settings.Service.DisablePowerOptions),
(factory.CreateNetworkOptionsConfiguration(groupId), Context.Configuration.Settings.Service.DisableNetworkOptions),
(factory.CreateRemoteConnectionConfiguration(groupId), Context.Configuration.Settings.Service.DisableRemoteConnections),
(factory.CreateSignoutConfiguration(groupId, sid, userName), Context.Configuration.Settings.Service.DisableSignout),
(factory.CreateSwitchUserConfiguration(groupId), Context.Configuration.Settings.Service.DisableUserSwitch),
(factory.CreateTaskManagerConfiguration(groupId, sid, userName), Context.Configuration.Settings.Service.DisableTaskManager),
(factory.CreateUserPowerOptionsConfiguration(groupId, sid, userName), Context.Configuration.Settings.Service.DisablePowerOptions),
(factory.CreateWindowsUpdateConfiguration(groupId), Context.Configuration.Settings.Service.DisableWindowsUpdate)
};
if (Context.Configuration.Settings.Service.SetVmwareConfiguration)
{
configurations.Add((factory.CreateVmwareOverlayConfiguration(groupId, sid, userName), Context.Configuration.Settings.Service.DisableVmwareOverlay));
}
logger.Info($"Attempting to perform lockdown (feature configuration group: {groupId})...");
foreach (var (configuration, disable) in configurations)
{
success &= TrySet(configuration, disable);
if (!success)
{
break;
}
}
if (success)
{
monitor.Start();
logger.Info("Lockdown successful.");
}
else
{
logger.Error("Lockdown was not successful!");
}
return success ? OperationResult.Success : OperationResult.Failed;
}
public override OperationResult Revert()
{
logger.Info($"Attempting to revert lockdown (feature configuration group: {groupId})...");
var configurations = backup.GetBy(groupId);
var success = true;
monitor.Reset();
foreach (var configuration in configurations)
{
success &= TryRestore(configuration);
}
if (success)
{
logger.Info("Lockdown reversion successful.");
}
else
{
logger.Warn("Lockdown reversion was not successful!");
}
return success ? OperationResult.Success : OperationResult.Failed;
}
private bool TryRestore(IFeatureConfiguration configuration)
{
var success = configuration.Restore();
if (success)
{
backup.Delete(configuration);
}
else
{
logger.Error($"Failed to restore {configuration}!");
}
return success;
}
private bool TrySet(IFeatureConfiguration configuration, bool disable)
{
var success = false;
var status = FeatureConfigurationStatus.Undefined;
configuration.Initialize();
backup.Save(configuration);
if (disable)
{
success = configuration.DisableFeature();
status = FeatureConfigurationStatus.Disabled;
}
else
{
success = configuration.EnableFeature();
status = FeatureConfigurationStatus.Enabled;
}
if (success)
{
monitor.Observe(configuration, status);
}
else
{
logger.Error($"Failed to configure {configuration}!");
}
return success;
}
}
}

View File

@@ -0,0 +1,48 @@
/*
* 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 SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.Lockdown.Contracts;
using SafeExamBrowser.Logging.Contracts;
namespace SafeExamBrowser.Service.Operations
{
internal class RestoreOperation : IOperation
{
private readonly IFeatureConfigurationBackup backup;
private ILogger logger;
private readonly SessionContext sessionContext;
public event ActionRequiredEventHandler ActionRequired { add { } remove { } }
public event StatusChangedEventHandler StatusChanged { add { } remove { } }
public RestoreOperation(IFeatureConfigurationBackup backup, ILogger logger, SessionContext sessionContext)
{
this.backup = backup;
this.logger = logger;
this.sessionContext = sessionContext;
}
public OperationResult Perform()
{
logger.Info("Starting auto-restore mechanism...");
sessionContext.AutoRestoreMechanism.Start();
return OperationResult.Success;
}
public OperationResult Revert()
{
logger.Info("Stopping auto-restore mechanism...");
sessionContext.AutoRestoreMechanism.Stop();
return OperationResult.Success;
}
}
}

View File

@@ -0,0 +1,46 @@
/*
* 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 SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.Logging.Contracts;
namespace SafeExamBrowser.Service.Operations
{
internal class ServiceEventCleanupOperation : IOperation
{
private ILogger logger;
private SessionContext sessionContext;
public event ActionRequiredEventHandler ActionRequired { add { } remove { } }
public event StatusChangedEventHandler StatusChanged { add { } remove { } }
public ServiceEventCleanupOperation(ILogger logger, SessionContext sessionContext)
{
this.logger = logger;
this.sessionContext = sessionContext;
}
public OperationResult Perform()
{
return OperationResult.Success;
}
public OperationResult Revert()
{
if (sessionContext.ServiceEvent != null)
{
logger.Info("Closing service event...");
sessionContext.ServiceEvent.Close();
logger.Info("Service event successfully closed.");
}
return OperationResult.Success;
}
}
}

View File

@@ -0,0 +1,46 @@
/*
* 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 SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Logging.Contracts;
namespace SafeExamBrowser.Service.Operations
{
internal class SessionActivationOperation : SessionOperation
{
private ILogger logger;
public SessionActivationOperation(ILogger logger, SessionContext sessionContext) : base(sessionContext)
{
this.logger = logger;
}
public override OperationResult Perform()
{
var success = Context.ServiceEvent.Set();
if (success)
{
logger.Info("Successfully informed runtime about new session activation.");
}
else
{
logger.Error("Failed to inform runtime about new session activation!");
}
Context.IsRunning = success;
return success ? OperationResult.Success : OperationResult.Failed;
}
public override OperationResult Revert()
{
return OperationResult.Success;
}
}
}

View File

@@ -0,0 +1,85 @@
/*
* 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.Threading;
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Logging.Contracts;
namespace SafeExamBrowser.Service.Operations
{
internal class SessionInitializationOperation : SessionOperation
{
private ILogger logger;
private Func<string, EventWaitHandle> serviceEventFactory;
public SessionInitializationOperation(ILogger logger, Func<string, EventWaitHandle> serviceEventFactory, SessionContext sessionContext) : base(sessionContext)
{
this.logger = logger;
this.serviceEventFactory = serviceEventFactory;
}
public override OperationResult Perform()
{
logger.Info("Initializing new session...");
logger.Info($" -> Client-ID: {Context.Configuration.AppConfig.ClientId}");
logger.Info($" -> Runtime-ID: {Context.Configuration.AppConfig.RuntimeId}");
logger.Info($" -> Session-ID: {Context.Configuration.SessionId}");
logger.Info("Stopping auto-restore mechanism...");
Context.AutoRestoreMechanism.Stop();
InitializeServiceEvent();
return OperationResult.Success;
}
public override OperationResult Revert()
{
var success = true;
var wasRunning = Context.IsRunning;
logger.Info("Starting auto-restore mechanism...");
Context.AutoRestoreMechanism.Start();
logger.Info("Clearing session data...");
Context.Configuration = null;
Context.IsRunning = false;
if (Context.ServiceEvent != null && wasRunning)
{
success = Context.ServiceEvent.Set();
if (success)
{
logger.Info("Successfully informed runtime about session termination.");
}
else
{
logger.Error("Failed to inform runtime about session termination!");
}
}
return success ? OperationResult.Success : OperationResult.Failed;
}
private void InitializeServiceEvent()
{
if (Context.ServiceEvent != null)
{
logger.Info("Closing service event from previous session...");
Context.ServiceEvent.Close();
logger.Info("Service event successfully closed.");
}
logger.Info("Attempting to create new service event...");
Context.ServiceEvent = serviceEventFactory.Invoke(Context.Configuration.AppConfig.ServiceEventName);
logger.Info("Service event successfully created.");
}
}
}

View File

@@ -0,0 +1,32 @@
/*
* 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 SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
namespace SafeExamBrowser.Service.Operations
{
/// <summary>
/// The base implementation to be used for all operations in the session operation sequence.
/// </summary>
internal abstract class SessionOperation : IOperation
{
protected SessionContext Context { get; private set; }
public event ActionRequiredEventHandler ActionRequired { add { } remove { } }
public event StatusChangedEventHandler StatusChanged { add { } remove { } }
public SessionOperation(SessionContext sessionContext)
{
Context = sessionContext;
}
public abstract OperationResult Perform();
public abstract OperationResult Revert();
}
}

View File

@@ -0,0 +1,35 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("SafeExamBrowser.Service")]
[assembly: AssemblyDescription("Safe Exam Browser")]
[assembly: AssemblyCompany("ETH Zürich")]
[assembly: AssemblyProduct("SafeExamBrowser.Service")]
[assembly: AssemblyCopyright("Copyright © 2024 ETH Zürich, IT Services")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
[assembly: InternalsVisibleTo("SafeExamBrowser.Service.UnitTests")]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("fa3c6692-dfed-4afa-bd58-9a3da2753c78")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0.0")]

View File

@@ -0,0 +1,134 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{FA3C6692-DFED-4AFA-BD58-9A3DA2753C78}</ProjectGuid>
<OutputType>WinExe</OutputType>
<RootNamespace>SafeExamBrowser.Service</RootNamespace>
<AssemblyName>SafeExamBrowser.Service</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x86\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x86</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>true</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<OutputPath>bin\x86\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x86</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>true</Prefer32Bit>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>SafeExamBrowser.ico</ApplicationIcon>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x64\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x64</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>true</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<OutputPath>bin\x64\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x64</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>true</Prefer32Bit>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Configuration.Install" />
<Reference Include="System.ServiceProcess" />
</ItemGroup>
<ItemGroup>
<Compile Include="Communication\ServiceHost.cs" />
<Compile Include="CompositionRoot.cs" />
<Compile Include="Installer.cs">
<SubType>Component</SubType>
</Compile>
<Compile Include="Operations\LockdownOperation.cs" />
<Compile Include="Operations\RestoreOperation.cs" />
<Compile Include="Operations\ServiceEventCleanupOperation.cs" />
<Compile Include="Operations\SessionActivationOperation.cs" />
<Compile Include="Operations\SessionInitializationOperation.cs" />
<Compile Include="Operations\SessionOperation.cs" />
<Compile Include="Service.cs">
<SubType>Component</SubType>
</Compile>
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ServiceController.cs" />
<Compile Include="SessionContext.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SafeExamBrowser.Communication.Contracts\SafeExamBrowser.Communication.Contracts.csproj">
<Project>{0cd2c5fe-711a-4c32-afe0-bb804fe8b220}</Project>
<Name>SafeExamBrowser.Communication.Contracts</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.Communication\SafeExamBrowser.Communication.csproj">
<Project>{c9416a62-0623-4d38-96aa-92516b32f02f}</Project>
<Name>SafeExamBrowser.Communication</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.Configuration.Contracts\SafeExamBrowser.Configuration.Contracts.csproj">
<Project>{7d74555e-63e1-4c46-bd0a-8580552368c8}</Project>
<Name>SafeExamBrowser.Configuration.Contracts</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.Core.Contracts\SafeExamBrowser.Core.Contracts.csproj">
<Project>{fe0e1224-b447-4b14-81e7-ed7d84822aa0}</Project>
<Name>SafeExamBrowser.Core.Contracts</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.Core\SafeExamBrowser.Core.csproj">
<Project>{3d6fdbb6-a4af-4626-bb2b-bf329d44f9cc}</Project>
<Name>SafeExamBrowser.Core</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.Lockdown.Contracts\SafeExamBrowser.Lockdown.Contracts.csproj">
<Project>{3368b17d-6060-4482-9983-aa800d74041d}</Project>
<Name>SafeExamBrowser.Lockdown.Contracts</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.Lockdown\SafeExamBrowser.Lockdown.csproj">
<Project>{386b6042-3e12-4753-9fc6-c88ea4f97030}</Project>
<Name>SafeExamBrowser.Lockdown</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.Logging.Contracts\SafeExamBrowser.Logging.Contracts.csproj">
<Project>{64ea30fb-11d4-436a-9c2b-88566285363e}</Project>
<Name>SafeExamBrowser.Logging.Contracts</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.Logging\SafeExamBrowser.Logging.csproj">
<Project>{e107026c-2011-4552-a7d8-3a0d37881df6}</Project>
<Name>SafeExamBrowser.Logging</Name>
</ProjectReference>
<ProjectReference Include="..\SafeExamBrowser.Settings\SafeExamBrowser.Settings.csproj">
<Project>{30b2d907-5861-4f39-abad-c4abf1b3470e}</Project>
<Name>SafeExamBrowser.Settings</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Content Include="SafeExamBrowser.ico" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

View File

@@ -0,0 +1,49 @@
/*
* 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.ServiceProcess;
namespace SafeExamBrowser.Service
{
public class Service : ServiceBase
{
private CompositionRoot instances;
public Service()
{
CanPauseAndContinue = false;
ServiceName = nameof(SafeExamBrowser);
}
public static void Main()
{
Run(new Service());
}
protected override void OnStart(string[] args)
{
instances = new CompositionRoot();
instances.BuildObjectGraph();
instances.LogStartupInformation();
var success = instances.ServiceController.TryStart();
if (!success)
{
Environment.Exit(-1);
}
}
protected override void OnStop()
{
instances?.ServiceController?.Terminate();
instances?.LogShutdownInformation();
}
}
}

View File

@@ -0,0 +1,227 @@
/*
* 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 SafeExamBrowser.Communication.Contracts.Events;
using SafeExamBrowser.Communication.Contracts.Hosts;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Core.Contracts.OperationModel;
using SafeExamBrowser.Lockdown.Contracts;
using SafeExamBrowser.Logging.Contracts;
namespace SafeExamBrowser.Service
{
internal class ServiceController
{
private ILogger logger;
private Func<string, ILogObserver> logWriterFactory;
private IOperationSequence bootstrapSequence;
private IOperationSequence sessionSequence;
private IServiceHost serviceHost;
private SessionContext sessionContext;
private ISystemConfigurationUpdate systemConfigurationUpdate;
private ILogObserver sessionWriter;
private ServiceConfiguration Session
{
get { return sessionContext.Configuration; }
}
private bool SessionIsRunning
{
get { return sessionContext.IsRunning; }
}
internal ServiceController(
ILogger logger,
Func<string, ILogObserver> logWriterFactory,
IOperationSequence bootstrapSequence,
IOperationSequence sessionSequence,
IServiceHost serviceHost,
SessionContext sessionContext,
ISystemConfigurationUpdate systemConfigurationUpdate)
{
this.logger = logger;
this.logWriterFactory = logWriterFactory;
this.bootstrapSequence = bootstrapSequence;
this.sessionSequence = sessionSequence;
this.serviceHost = serviceHost;
this.sessionContext = sessionContext;
this.systemConfigurationUpdate = systemConfigurationUpdate;
}
internal bool TryStart()
{
logger.Info("Initiating startup procedure...");
var result = bootstrapSequence.TryPerform();
var success = result == OperationResult.Success;
if (success)
{
RegisterEvents();
logger.Info("Service successfully initialized.");
logger.Log(string.Empty);
}
else
{
logger.Info("Service startup aborted!");
logger.Log(string.Empty);
}
return success;
}
internal void Terminate()
{
DeregisterEvents();
if (SessionIsRunning)
{
StopSession();
}
logger.Log(string.Empty);
logger.Info("Initiating termination procedure...");
var result = bootstrapSequence.TryRevert();
var success = result == OperationResult.Success;
if (success)
{
logger.Info("Service successfully terminated.");
logger.Log(string.Empty);
}
else
{
logger.Info("Service termination failed!");
logger.Log(string.Empty);
}
}
private void StartSession()
{
InitializeSessionLogging();
logger.Info(AppendDivider("Session Start Procedure"));
var result = sessionSequence.TryPerform();
if (result == OperationResult.Success)
{
logger.Info(AppendDivider("Session Running"));
}
else
{
logger.Info(AppendDivider("Session Start Failed"));
}
}
private void StopSession()
{
logger.Info(AppendDivider("Session Stop Procedure"));
var result = sessionSequence.TryRevert();
if (result == OperationResult.Success)
{
logger.Info(AppendDivider("Session Terminated"));
}
else
{
logger.Info(AppendDivider("Session Stop Failed"));
}
FinalizeSessionLogging();
}
private void RegisterEvents()
{
serviceHost.SessionStartRequested += ServiceHost_SessionStartRequested;
serviceHost.SessionStopRequested += ServiceHost_SessionStopRequested;
serviceHost.SystemConfigurationUpdateRequested += ServiceHost_SystemConfigurationUpdateRequested;
}
private void DeregisterEvents()
{
serviceHost.SessionStartRequested -= ServiceHost_SessionStartRequested;
serviceHost.SessionStopRequested -= ServiceHost_SessionStopRequested;
serviceHost.SystemConfigurationUpdateRequested -= ServiceHost_SystemConfigurationUpdateRequested;
}
private void ServiceHost_SessionStartRequested(SessionStartEventArgs args)
{
if (!SessionIsRunning)
{
sessionContext.Configuration = args.Configuration;
StartSession();
}
else
{
logger.Warn("Received session start request, even though a session is already running!");
}
}
private void ServiceHost_SessionStopRequested(SessionStopEventArgs args)
{
if (SessionIsRunning)
{
if (Session.SessionId == args.SessionId)
{
StopSession();
}
else
{
logger.Warn("Received session stop request with wrong session ID!");
}
}
else
{
logger.Warn("Received session stop request, even though no session is currently running!");
}
}
private void ServiceHost_SystemConfigurationUpdateRequested()
{
logger.Info("Received request to initiate system configuration update.");
systemConfigurationUpdate.ExecuteAsync();
}
private string AppendDivider(string message)
{
var dashesLeft = new String('-', 48 - message.Length / 2 - message.Length % 2);
var dashesRight = new String('-', 48 - message.Length / 2);
return $"### {dashesLeft} {message} {dashesRight} ###";
}
private void InitializeSessionLogging()
{
if (Session?.AppConfig?.ServiceLogFilePath != null)
{
sessionWriter = logWriterFactory.Invoke(Session.AppConfig.ServiceLogFilePath);
logger.Subscribe(sessionWriter);
logger.LogLevel = Session.Settings.LogLevel;
}
else
{
logger.Warn("Could not initialize session writer due to missing configuration data!");
}
}
private void FinalizeSessionLogging()
{
if (sessionWriter != null)
{
logger.Unsubscribe(sessionWriter);
sessionWriter = null;
}
}
}
}

View File

@@ -0,0 +1,40 @@
/*
* 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.Threading;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Lockdown.Contracts;
namespace SafeExamBrowser.Service
{
/// <summary>
/// Holds all configuration and runtime data required for the session handling.
/// </summary>
internal class SessionContext
{
/// <summary>
/// The mechanism trying to restore all changes after an application or system failure.
/// </summary>
internal IAutoRestoreMechanism AutoRestoreMechanism { get; set; }
/// <summary>
/// The configuration of the currently active session.
/// </summary>
internal ServiceConfiguration Configuration { get; set; }
/// <summary>
/// Indicates whether a session is currently running.
/// </summary>
internal bool IsRunning { get; set; }
/// <summary>
/// The global inter-process event used for status synchronization with the runtime component.
/// </summary>
internal EventWaitHandle ServiceEvent { get; set; }
}
}