// 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; } } } } } }