// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using EpicGames.Core; using EpicGames.Horde.Compute.Transports; using Microsoft.Extensions.Logging; namespace EpicGames.Horde.Compute.Clients { /// /// Implementation of which marshals data over a loopback connection to a method running on a background task in the same process. /// public sealed class LocalComputeClient : IComputeClient, IAsyncDisposable { private static readonly ClusterId s_cluster = new ("_local"); class LeaseImpl : IComputeLease { public ClusterId Cluster { get; } = s_cluster; public IReadOnlyList Properties { get; } = new List(); public IReadOnlyDictionary AssignedResources => new Dictionary(); public RemoteComputeSocket Socket => _socket; public string Ip => "127.0.0.1"; public ConnectionMode ConnectionMode => ConnectionMode.Direct; public IReadOnlyDictionary Ports => new Dictionary(); readonly RemoteComputeSocket _socket; public LeaseImpl(RemoteComputeSocket socket) => _socket = socket; /// public ValueTask DisposeAsync() => _socket.DisposeAsync(); /// public ValueTask CloseAsync(CancellationToken cancellationToken) => _socket.CloseAsync(cancellationToken); } readonly BackgroundTask _listenerTask; readonly Socket _listener; readonly Socket _socket; readonly bool _executeInProcess; /// /// Constructor /// /// Port to connect on /// Sandbox directory for the worker /// Whether to run external assemblies in-process. Useful for debugging. /// Logger for diagnostic output public LocalComputeClient(int port, DirectoryReference sandboxDir, bool executeInProcess, ILogger logger) { _executeInProcess = executeInProcess; _listener = new Socket(SocketType.Stream, ProtocolType.IP); _listener.Bind(new IPEndPoint(IPAddress.Loopback, port)); _listener.Listen(); _listenerTask = BackgroundTask.StartNew(ctx => RunListenerAsync(_listener, sandboxDir, _executeInProcess, logger, ctx)); _socket = new Socket(SocketType.Stream, ProtocolType.IP); _socket.Connect(IPAddress.Loopback, port); } /// public async ValueTask DisposeAsync() { _socket.Dispose(); await _listenerTask.DisposeAsync(); _listener.Dispose(); } /// /// Sets up the loopback listener and calls the server method /// static async Task RunListenerAsync(Socket listener, DirectoryReference sandboxDir, bool executeInProcess, ILogger logger, CancellationToken cancellationToken) { using Socket tcpSocket = await listener.AcceptAsync(cancellationToken); await using TcpTransport tcpTransport = new(tcpSocket); await using RemoteComputeSocket socket = new(tcpTransport, ComputeProtocol.Latest, logger); AgentMessageHandler worker = new(sandboxDir, null, executeInProcess, null, null, logger); await worker.RunAsync(socket, cancellationToken); await socket.CloseAsync(cancellationToken); } /// public Task GetClusterAsync(Requirements? requirements, string? requestId, ConnectionMetadataRequest? connection, ILogger logger, CancellationToken cancellationToken = default) { return Task.FromResult(s_cluster); } /// public Task TryAssignWorkerAsync(ClusterId? clusterId, Requirements? requirements, string? requestId, ConnectionMetadataRequest? connection, ILogger logger, CancellationToken cancellationToken) { #pragma warning disable CA2000 // Dispose objects before losing scope RemoteComputeSocket socket = new RemoteComputeSocket(new TcpTransport(_socket), ComputeProtocol.Latest, logger); return Task.FromResult(new LeaseImpl(socket)); #pragma warning restore CA2000 // Dispose objects before losing scope } /// public Task DeclareResourceNeedsAsync(ClusterId clusterId, string pool, Dictionary resourceNeeds, CancellationToken cancellationToken = default) { return Task.CompletedTask; } } }