Restore SEBPatch
This commit is contained in:
270
SafeExamBrowser.Runtime/Operations/ClientOperation.cs
Normal file
270
SafeExamBrowser.Runtime/Operations/ClientOperation.cs
Normal file
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
* 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.Text;
|
||||
using System.Threading;
|
||||
using SafeExamBrowser.Communication.Contracts;
|
||||
using SafeExamBrowser.Communication.Contracts.Events;
|
||||
using SafeExamBrowser.Communication.Contracts.Hosts;
|
||||
using SafeExamBrowser.Communication.Contracts.Proxies;
|
||||
using SafeExamBrowser.Core.Contracts.OperationModel;
|
||||
using SafeExamBrowser.Core.Contracts.OperationModel.Events;
|
||||
using SafeExamBrowser.I18n.Contracts;
|
||||
using SafeExamBrowser.Logging.Contracts;
|
||||
using SafeExamBrowser.WindowsApi.Contracts;
|
||||
using SafeExamBrowser.WindowsApi.Contracts.Events;
|
||||
|
||||
namespace SafeExamBrowser.Runtime.Operations
|
||||
{
|
||||
internal class ClientOperation : SessionOperation
|
||||
{
|
||||
private readonly ILogger logger;
|
||||
private readonly IProcessFactory processFactory;
|
||||
private readonly IProxyFactory proxyFactory;
|
||||
private readonly IRuntimeHost runtimeHost;
|
||||
private readonly int timeout_ms;
|
||||
|
||||
private IProcess ClientProcess
|
||||
{
|
||||
get { return Context.ClientProcess; }
|
||||
set { Context.ClientProcess = value; }
|
||||
}
|
||||
|
||||
private IClientProxy ClientProxy
|
||||
{
|
||||
get { return Context.ClientProxy; }
|
||||
set { Context.ClientProxy = value; }
|
||||
}
|
||||
|
||||
public override event ActionRequiredEventHandler ActionRequired { add { } remove { } }
|
||||
public override event StatusChangedEventHandler StatusChanged;
|
||||
|
||||
public ClientOperation(
|
||||
ILogger logger,
|
||||
IProcessFactory processFactory,
|
||||
IProxyFactory proxyFactory,
|
||||
IRuntimeHost runtimeHost,
|
||||
SessionContext sessionContext,
|
||||
int timeout_ms) : base(sessionContext)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.processFactory = processFactory;
|
||||
this.proxyFactory = proxyFactory;
|
||||
this.runtimeHost = runtimeHost;
|
||||
this.timeout_ms = timeout_ms;
|
||||
}
|
||||
|
||||
public override OperationResult Perform()
|
||||
{
|
||||
StatusChanged?.Invoke(TextKey.OperationStatus_StartClient);
|
||||
|
||||
var success = TryStartClient();
|
||||
|
||||
if (success)
|
||||
{
|
||||
logger.Info("Successfully started new client instance.");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error("Failed to start new client instance! Aborting procedure...");
|
||||
}
|
||||
|
||||
return success ? OperationResult.Success : OperationResult.Failed;
|
||||
}
|
||||
|
||||
public override OperationResult Repeat()
|
||||
{
|
||||
return Perform();
|
||||
}
|
||||
|
||||
public override OperationResult Revert()
|
||||
{
|
||||
var success = true;
|
||||
|
||||
if (ClientProcess != null && !ClientProcess.HasTerminated)
|
||||
{
|
||||
StatusChanged?.Invoke(TextKey.OperationStatus_StopClient);
|
||||
success = TryStopClient();
|
||||
}
|
||||
|
||||
return success ? OperationResult.Success : OperationResult.Failed;
|
||||
}
|
||||
|
||||
private bool TryStartClient()
|
||||
{
|
||||
var authenticationToken = Context.Next.ClientAuthenticationToken.ToString("D");
|
||||
var executablePath = Context.Next.AppConfig.ClientExecutablePath;
|
||||
var logFilePath = $"{'"' + Convert.ToBase64String(Encoding.UTF8.GetBytes(Context.Next.AppConfig.ClientLogFilePath)) + '"'}";
|
||||
var logLevel = Context.Next.Settings.LogLevel.ToString();
|
||||
var runtimeHostUri = Context.Next.AppConfig.RuntimeAddress;
|
||||
var uiMode = Context.Next.Settings.UserInterface.Mode.ToString();
|
||||
|
||||
var clientReady = false;
|
||||
var clientReadyEvent = new AutoResetEvent(false);
|
||||
var clientReadyEventHandler = new CommunicationEventHandler(() => clientReadyEvent.Set());
|
||||
|
||||
var clientTerminated = false;
|
||||
var clientTerminatedEventHandler = new ProcessTerminatedEventHandler(_ => { clientTerminated = true; clientReadyEvent.Set(); });
|
||||
|
||||
logger.Info("Starting new client process...");
|
||||
runtimeHost.AllowConnection = true;
|
||||
runtimeHost.AuthenticationToken = Context.Next.ClientAuthenticationToken;
|
||||
runtimeHost.ClientReady += clientReadyEventHandler;
|
||||
ClientProcess = processFactory.StartNew(executablePath, logFilePath, logLevel, runtimeHostUri, authenticationToken, uiMode);
|
||||
ClientProcess.Terminated += clientTerminatedEventHandler;
|
||||
|
||||
logger.Info("Waiting for client to complete initialization...");
|
||||
clientReady = clientReadyEvent.WaitOne();
|
||||
|
||||
runtimeHost.AllowConnection = false;
|
||||
runtimeHost.AuthenticationToken = default(Guid?);
|
||||
runtimeHost.ClientReady -= clientReadyEventHandler;
|
||||
ClientProcess.Terminated -= clientTerminatedEventHandler;
|
||||
|
||||
if (clientReady && !clientTerminated)
|
||||
{
|
||||
return TryStartCommunication();
|
||||
}
|
||||
|
||||
if (!clientReady)
|
||||
{
|
||||
logger.Error($"Failed to start client!");
|
||||
}
|
||||
|
||||
if (clientTerminated)
|
||||
{
|
||||
logger.Error("Client instance terminated unexpectedly during initialization!");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryStartCommunication()
|
||||
{
|
||||
var success = false;
|
||||
|
||||
logger.Info("Client has been successfully started and initialized. Creating communication proxy for client host...");
|
||||
ClientProxy = proxyFactory.CreateClientProxy(Context.Next.AppConfig.ClientAddress, Interlocutor.Runtime);
|
||||
|
||||
if (ClientProxy.Connect(Context.Next.ClientAuthenticationToken))
|
||||
{
|
||||
logger.Info("Connection with client has been established. Requesting authentication...");
|
||||
|
||||
var communication = ClientProxy.RequestAuthentication();
|
||||
var response = communication.Value;
|
||||
|
||||
success = communication.Success && ClientProcess.Id == response?.ProcessId;
|
||||
|
||||
if (success)
|
||||
{
|
||||
logger.Info("Authentication of client has been successful, client is ready to operate.");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error("Failed to verify client integrity!");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error("Failed to connect to client!");
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
private bool TryStopClient()
|
||||
{
|
||||
var success = false;
|
||||
|
||||
var disconnected = false;
|
||||
var disconnectedEvent = new AutoResetEvent(false);
|
||||
var disconnectedEventHandler = new CommunicationEventHandler(() => disconnectedEvent.Set());
|
||||
|
||||
var terminated = false;
|
||||
var terminatedEvent = new AutoResetEvent(false);
|
||||
var terminatedEventHandler = new ProcessTerminatedEventHandler((_) => terminatedEvent.Set());
|
||||
|
||||
if (ClientProxy != null)
|
||||
{
|
||||
runtimeHost.ClientDisconnected += disconnectedEventHandler;
|
||||
ClientProcess.Terminated += terminatedEventHandler;
|
||||
|
||||
logger.Info("Instructing client to initiate shutdown procedure.");
|
||||
ClientProxy.InitiateShutdown();
|
||||
|
||||
logger.Info("Disconnecting from client communication host.");
|
||||
ClientProxy.Disconnect();
|
||||
|
||||
logger.Info("Waiting for client to disconnect from runtime communication host...");
|
||||
disconnected = disconnectedEvent.WaitOne(timeout_ms / 2);
|
||||
|
||||
if (!disconnected)
|
||||
{
|
||||
logger.Error($"Client failed to disconnect within {timeout_ms / 2 / 1000} seconds!");
|
||||
}
|
||||
|
||||
logger.Info("Waiting for client process to terminate...");
|
||||
terminated = terminatedEvent.WaitOne(timeout_ms / 2);
|
||||
|
||||
if (!terminated)
|
||||
{
|
||||
logger.Error($"Client failed to terminate within {timeout_ms / 2 / 1000} seconds!");
|
||||
}
|
||||
|
||||
runtimeHost.ClientDisconnected -= disconnectedEventHandler;
|
||||
ClientProcess.Terminated -= terminatedEventHandler;
|
||||
}
|
||||
|
||||
if (disconnected && terminated)
|
||||
{
|
||||
logger.Info("Client has been successfully terminated.");
|
||||
success = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Warn("Attempting to kill client process since graceful termination failed!");
|
||||
success = TryKillClient();
|
||||
}
|
||||
|
||||
if (success)
|
||||
{
|
||||
ClientProcess = null;
|
||||
ClientProxy = null;
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
private bool TryKillClient()
|
||||
{
|
||||
const int MAX_ATTEMPTS = 5;
|
||||
|
||||
for (var attempt = 1; attempt <= MAX_ATTEMPTS; attempt++)
|
||||
{
|
||||
logger.Info($"Attempt {attempt}/{MAX_ATTEMPTS} to kill client process with ID = {ClientProcess.Id}.");
|
||||
|
||||
if (ClientProcess.TryKill(500))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ClientProcess.HasTerminated)
|
||||
{
|
||||
logger.Info("Client process has terminated.");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error($"Failed to kill client process within {MAX_ATTEMPTS} attempts!");
|
||||
}
|
||||
|
||||
return ClientProcess.HasTerminated;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user