// Copyright Epic Games, Inc. All Rights Reserved.
using System.IO.Pipes;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security.AccessControl;
using System.Security.Principal;
using EpicGames.Core;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace HordeAgent.Services
{
///
/// Tracks the current status of the agent
///
class StatusService : IHostedService, IAsyncDisposable
{
const int NumPipes = 10;
private AgentStatusMessage _current;
private bool _isBusy;
private bool _isStopRequested;
readonly IOptionsMonitor _settings;
readonly BackgroundTask _task;
readonly ILogger _logger;
///
/// The current agent status
///
public AgentStatusMessage Current => _current;
///
/// Whether the agent is busy performing other work.
///
public bool IsBusy
{
get => _isBusy;
set
{
if (_isBusy != value)
{
_isBusy = value;
StatusChangedEvent.Set();
}
}
}
///
/// Whether a stop of the agent is requested
///
public bool IsStopRequested
{
get => _isStopRequested;
set
{
if (_isStopRequested != value)
{
_isStopRequested = value;
StatusChangedEvent.Set();
}
}
}
///
/// Status was updated
///
public readonly AsyncEvent StatusChangedEvent = new();
///
/// Constructor
///
public StatusService(IOptionsMonitor settings, ILogger logger)
{
_current = new AgentStatusMessage(true, 0, AgentStatusMessage.Starting);
_settings = settings;
_task = new BackgroundTask(RunPipeServerAsync);
_logger = logger;
}
///
public async ValueTask DisposeAsync()
{
await _task.DisposeAsync();
}
///
public Task StartAsync(CancellationToken cancellationToken)
{
_task.Start();
return Task.CompletedTask;
}
///
public async Task StopAsync(CancellationToken cancellationToken)
{
await _task.StopAsync(cancellationToken);
}
///
/// Sets the current status
///
public void Set(AgentStatusMessage status)
{
Interlocked.Exchange(ref _current, status);
_logger.LogDebug("Updating status: {@Status}", status);
}
///
/// Sets the current status
///
public void Set(bool healthy, int numLeases, string message) => Set(new AgentStatusMessage(healthy, numLeases, message));
///
/// Sets a status description
///
public void SetDescription(string description) => Update(status => new AgentStatusMessage(status.Healthy, status.NumLeases, description));
///
/// Updates the status using a custom function
///
/// Function to take the existing status and create an updated version
public void Update(Func updateFunc)
{
for (; ; )
{
AgentStatusMessage info = _current;
if (Interlocked.CompareExchange(ref _current, updateFunc(info), info) == info)
{
break;
}
}
}
private async Task RunPipeServerAsync(CancellationToken cancellationToken)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
List tasks = new List();
for (int idx = 0; idx < NumPipes; idx++)
{
tasks.Add(RunSinglePipeServerAsync(cancellationToken));
}
await Task.WhenAll(tasks);
}
}
[SupportedOSPlatform("windows")]
private async Task RunSinglePipeServerAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
try
{
await RunPipeServerInternalAsync(cancellationToken);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
break;
}
catch (UnauthorizedAccessException e)
{
_logger.LogError("Unable to start IPC server. Ensure no other Horde agent processes are running! Reason: {Reason}", e.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception while running pipe server: {Message}", ex.Message);
}
await Task.Delay(TimeSpan.FromSeconds(30), cancellationToken);
}
}
AgentSettingsMessage GetSettingsMessage()
{
AgentSettings settings = _settings.CurrentValue;
ServerProfile profile = settings.GetCurrentServerProfile();
return new AgentSettingsMessage(profile.Url);
}
[SupportedOSPlatform("windows")]
private async Task RunPipeServerInternalAsync(CancellationToken cancellationToken)
{
AgentMessageBuffer request = new AgentMessageBuffer();
AgentMessageBuffer response = new AgentMessageBuffer();
_logger.LogDebug("Creating pipe for status updates");
PipeSecurity pipeSecurity = new PipeSecurity();
pipeSecurity.AddAccessRule(new PipeAccessRule(WindowsIdentity.GetCurrent().Name, PipeAccessRights.FullControl, AccessControlType.Allow));
IdentityReference usersReference = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null).Translate(typeof(NTAccount));
string users = usersReference.ToString().Replace(@"builtin\", "", StringComparison.InvariantCultureIgnoreCase);
pipeSecurity.AddAccessRule(new PipeAccessRule(users, PipeAccessRights.ReadWrite, AccessControlType.Allow));
using (NamedPipeServerStream pipeServer = NamedPipeServerStreamAcl.Create(AgentMessagePipe.PipeName, PipeDirection.InOut, NumPipes, PipeTransmissionMode.Byte, PipeOptions.Asynchronous, 0, 0, pipeSecurity))
{
await pipeServer.WaitForConnectionAsync(cancellationToken);
_logger.LogDebug("Received pipe connection");
while (await request.TryReadAsync(pipeServer, cancellationToken))
{
switch (request.Type)
{
case AgentMessageType.SetEnabledRequest:
IsBusy = !request.Parse().IsEnabled;
break;
case AgentMessageType.GetStatusRequest:
response.Set(AgentMessageType.GetStatusResponse, Current);
await response.SendAsync(pipeServer, cancellationToken);
break;
case AgentMessageType.SetSettingsRequest:
AgentSetSettingsRequest req = request.Parse();
_settings.CurrentValue.CpuCount = req.CpuCount;
_settings.CurrentValue.CpuMultiplier = req.CpuMultiplier;
break;
case AgentMessageType.GetSettingsRequest:
response.Set(AgentMessageType.GetSettingsResponse, GetSettingsMessage());
await response.SendAsync(pipeServer, cancellationToken);
break;
default:
response.Set(AgentMessageType.InvalidResponse);
await response.SendAsync(pipeServer, cancellationToken);
break;
}
}
}
}
}
}