// Copyright Epic Games, Inc. All Rights Reserved. using System.Collections.Generic; using System.Diagnostics; using System.Net; using System.Net.Sockets; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using EpicGames.Core; using EpicGames.Horde.Compute.Transports; using Microsoft.Extensions.Logging; namespace EpicGames.Horde.Compute.Clients { /// /// Runs a local Horde Agent process to process compute requests without communicating with a server /// public sealed class AgentComputeClient : IComputeClient { private static readonly ClusterId s_cluster = new ("_agent"); class LeaseImpl : IComputeLease { readonly IAsyncEnumerator _source; /// public ClusterId Cluster { get; } = s_cluster; /// public IReadOnlyList Properties { get; } = new List(); /// public IReadOnlyDictionary AssignedResources => new Dictionary(); /// public RemoteComputeSocket Socket => _source.Current; /// public string Ip => "127.0.0.1"; /// public UbaConfig? Uba => null; /// public ConnectionMode ConnectionMode => ConnectionMode.Direct; /// public IReadOnlyDictionary Ports => new Dictionary(); public LeaseImpl(IAsyncEnumerator source) => _source = source; /// public async ValueTask DisposeAsync() { await _source.MoveNextAsync(); await _source.DisposeAsync(); } /// public ValueTask CloseAsync(CancellationToken cancellationToken) => Socket.CloseAsync(cancellationToken); } readonly string _hordeAgentAssembly; readonly int _port; readonly ILogger _logger; /// /// Constructor /// /// Path to the Horde Agent assembly /// Loopback port to connect on /// Factory for logger instances public AgentComputeClient(string hordeAgentAssembly, int port, ILogger logger) { _hordeAgentAssembly = hordeAgentAssembly; _port = port; _logger = logger; } /// public Task GetClusterAsync(Requirements? requirements, string? requestId, ConnectionMetadataRequest? connection, ILogger logger, CancellationToken cancellationToken = default) { return Task.FromResult(s_cluster); } /// public async Task TryAssignWorkerAsync(ClusterId? clusterId, Requirements? requirements, string? requestId, ConnectionMetadataRequest? connection, bool? useUbaCache, ILogger logger, CancellationToken cancellationToken) { logger.LogInformation("** CLIENT **"); logger.LogInformation("Launching {Path} to handle remote", _hordeAgentAssembly); // The connection logic is an async enumerator that returns the socket, then shuts down. IAsyncEnumerator source = ConnectAsync(logger, cancellationToken).GetAsyncEnumerator(cancellationToken); if (!await source.MoveNextAsync()) { await source.DisposeAsync(); return null; } return new LeaseImpl(source); } /// public Task DeclareResourceNeedsAsync(ClusterId clusterId, string pool, Dictionary resourceNeeds, CancellationToken cancellationToken = default) { return Task.CompletedTask; } /// public Task AllocateUbaCacheServerAsync(ClusterId clusterId, CancellationToken cancellationToken = default) { throw new System.NotImplementedException(); } async IAsyncEnumerable ConnectAsync(ILogger logger, [EnumeratorCancellation] CancellationToken cancellationToken) { using Socket listener = new Socket(SocketType.Stream, ProtocolType.IP); listener.Bind(new IPEndPoint(IPAddress.Loopback, _port)); listener.Listen(); await using BackgroundTask agentTask = BackgroundTask.StartNew(ctx => RunAgentAsync(_hordeAgentAssembly, _port, logger, ctx)); using Socket tcpSocket = await listener.AcceptAsync(cancellationToken); await using TcpTransport tcpTransport = new(tcpSocket); await using RemoteComputeSocket socket = new(tcpTransport, ComputeProtocol.Latest, _logger); yield return socket; await socket.CloseAsync(cancellationToken); } static async Task RunAgentAsync(string hordeAgentAssembly, int port, ILogger logger, CancellationToken cancellationToken) { using ManagedProcessGroup group = new ManagedProcessGroup(); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { List arguments = new List(); arguments.Add(hordeAgentAssembly); arguments.Add("computeworker"); arguments.Add($"-port={port}"); using ManagedProcess process = new ManagedProcess(group, "dotnet", CommandLineArguments.Join(arguments), null, null, ProcessPriorityClass.Normal); string? line; while ((line = await process.ReadLineAsync(cancellationToken)) != null) { logger.LogInformation("Output: {Line}", line); } await process.WaitForExitAsync(cancellationToken); } else { using Process process = new Process(); process.StartInfo.FileName = "dotnet"; process.StartInfo.ArgumentList.Add(hordeAgentAssembly); process.StartInfo.ArgumentList.Add("computeworker"); process.StartInfo.ArgumentList.Add($"-port={port}"); process.StartInfo.UseShellExecute = true; process.Start(); await process.WaitForExitAsync(cancellationToken); } } } }