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