2023-03-24 10:32:39 -04:00
// Copyright Epic Games, Inc. All Rights Reserved.
2023-04-10 14:41:16 -04:00
using System.Buffers.Binary ;
2023-03-24 10:32:39 -04:00
using System.Reflection ;
using EpicGames.Core ;
2023-09-18 19:24:22 -04:00
using EpicGames.Horde ;
2023-03-29 13:03:35 -04:00
using EpicGames.Horde.Common ;
2023-03-24 10:32:39 -04:00
using EpicGames.Horde.Compute ;
using EpicGames.Horde.Compute.Clients ;
2023-10-10 09:01:45 -04:00
using EpicGames.Horde.Storage ;
2023-06-24 16:37:55 -04:00
using EpicGames.Horde.Storage.Bundles ;
2023-09-09 10:14:12 -04:00
using EpicGames.Horde.Storage.Clients ;
2023-03-24 10:32:39 -04:00
using EpicGames.Horde.Storage.Nodes ;
2023-09-18 19:24:22 -04:00
using Microsoft.Extensions.DependencyInjection ;
2023-03-24 10:32:39 -04:00
using Microsoft.Extensions.Logging ;
namespace RemoteClient
{
2023-03-29 13:03:35 -04:00
class ClientAppOptions
{
[CommandLine("-Server=")]
public string? Server { get ; set ; }
[CommandLine("-Oidc=")]
public string? OidcProvider { get ; set ; }
[CommandLine("-Condition=")]
public string? Condition { get ; set ; }
2023-03-30 15:46:03 -04:00
2023-07-28 11:51:34 -04:00
[CommandLine]
public bool InProc { get ; set ; }
2023-03-30 15:46:03 -04:00
[CommandLine("-Cpp")]
public bool UseCppWorker { get ; set ; }
2023-03-29 13:03:35 -04:00
}
2023-03-24 10:32:39 -04:00
class ClientApp
{
2023-03-29 13:03:35 -04:00
static FileReference CurrentAssemblyFile { get ; } = new FileReference ( Assembly . GetExecutingAssembly ( ) . Location ) ;
static DirectoryReference ClientSourceDir { get ; } = DirectoryReference . Combine ( CurrentAssemblyFile . Directory , "../../.." ) ;
2023-03-24 10:32:39 -04:00
2023-03-29 13:03:35 -04:00
static async Task Main ( string [ ] args )
2023-03-24 10:32:39 -04:00
{
2023-03-29 13:03:35 -04:00
ILogger logger = new DefaultConsoleLogger ( LogLevel . Trace ) ;
// Parse the command line arguments
ClientAppOptions options = new ClientAppOptions ( ) ;
CommandLineArguments arguments = new CommandLineArguments ( args ) ;
arguments . ApplyTo ( options ) ;
arguments . CheckAllArgumentsUsed ( logger ) ;
2023-03-24 10:32:39 -04:00
2023-09-18 19:24:22 -04:00
// Create a DI container that can create and authenticate Horde HTTP clients for us
ServiceCollection services = new ServiceCollection ( ) ;
if ( options . Server = = null )
{
DirectoryReference sandboxDir = DirectoryReference . Combine ( new FileReference ( Assembly . GetExecutingAssembly ( ) . Location ) . Directory , "Sandbox" ) ;
services . AddSingleton < IComputeClient > ( sp = > new LocalComputeClient ( 2000 , sandboxDir , options . InProc , new PrefixLogger ( "[REMOTE]" , logger ) ) ) ;
}
else
{
2024-05-17 09:05:02 -04:00
services . AddHorde ( x = > x . ServerUrl = new Uri ( options . Server ) ) ;
2023-09-18 19:24:22 -04:00
services . AddSingleton < IComputeClient , ServerComputeClient > ( ) ;
}
2023-03-24 12:41:04 -04:00
// Create the client to handle our requests
2023-09-18 19:24:22 -04:00
await using ServiceProvider serviceProvider = services . BuildServiceProvider ( ) ;
IComputeClient client = serviceProvider . GetRequiredService < IComputeClient > ( ) ;
2023-03-24 12:41:04 -04:00
// Allocate a worker
2023-03-29 13:03:35 -04:00
Requirements ? requirements = null ;
if ( options . Condition ! = null )
{
requirements = new Requirements ( Condition . Parse ( options . Condition ) ) ;
}
2023-11-14 07:59:05 -05:00
await using IComputeLease ? lease = await client . TryAssignWorkerAsync ( new ClusterId ( "default" ) , requirements , null , null , new PrefixLogger ( "[CLIENT]" , logger ) ) ;
2023-03-24 12:41:04 -04:00
if ( lease = = null )
{
logger . LogInformation ( "Unable to connect to remote" ) ;
return ;
}
2023-03-30 15:46:03 -04:00
// Run the worker
if ( options . UseCppWorker )
{
FileReference remoteServerFile = FileReference . Combine ( ClientSourceDir , "../RemoteWorkerCpp/bin/RemoteServerCpp.exe" ) ;
2023-11-09 15:26:12 -05:00
await RunRemoteAsync ( lease , remoteServerFile . Directory , "RemoteWorkerCpp.exe" , new List < string > ( ) , logger ) ;
2023-03-30 15:46:03 -04:00
}
else
{
FileReference remoteServerFile = FileReference . Combine ( ClientSourceDir , "../RemoteWorker" , CurrentAssemblyFile . Directory . MakeRelativeTo ( ClientSourceDir ) , "RemoteWorker.dll" ) ;
2023-11-09 15:26:12 -05:00
await RunRemoteAsync ( lease , remoteServerFile . Directory , @"C:\Program Files\dotnet\dotnet.exe" , new List < string > { remoteServerFile . GetFileName ( ) } , logger ) ;
2023-03-30 15:46:03 -04:00
}
}
2023-04-20 16:15:10 -04:00
const int PrimaryChannelId = 0 ;
const int BackgroundChannelId = 1 ;
2023-05-19 21:50:03 -04:00
const int ChildProcessChannelId = 100 ;
2023-04-20 16:15:10 -04:00
2023-11-09 15:26:12 -05:00
static async Task RunRemoteAsync ( IComputeLease lease , DirectoryReference uploadDir , string executable , List < string > arguments , ILogger logger )
2023-03-30 15:46:03 -04:00
{
2023-03-24 12:41:04 -04:00
// Create a message channel on channel id 0. The Horde Agent always listens on this channel for requests.
2023-08-21 14:38:59 -04:00
using ( AgentMessageChannel channel = lease . Socket . CreateAgentMessageChannel ( PrimaryChannelId , 4 * 1024 * 1024 ) )
2023-03-24 12:41:04 -04:00
{
2023-04-11 15:00:24 -04:00
await channel . WaitForAttachAsync ( ) ;
2023-03-24 12:41:04 -04:00
2023-05-19 21:50:03 -04:00
// Fork another message loop. We'll use this to run an XOR task in the background.
2023-08-21 14:38:59 -04:00
using AgentMessageChannel backgroundChannel = lease . Socket . CreateAgentMessageChannel ( BackgroundChannelId , 4 * 1024 * 1024 ) ;
2023-04-20 15:15:34 -04:00
await using BackgroundTask otherChannelTask = BackgroundTask . StartNew ( ctx = > RunBackgroundXorAsync ( backgroundChannel ) ) ;
await channel . ForkAsync ( BackgroundChannelId , 4 * 1024 * 1024 , default ) ;
// Upload the sandbox to the primary channel.
2024-09-10 10:26:02 -04:00
BundleStorageNamespace storage = BundleStorageNamespace . CreateInMemory ( logger ) ;
2023-11-09 15:26:12 -05:00
2024-01-17 15:55:37 -05:00
await using ( IBlobWriter writer = storage . CreateBlobWriter ( ) )
2023-03-29 13:03:35 -04:00
{
2024-07-25 15:09:26 -04:00
IHashedBlobRef < DirectoryNode > sandbox = await writer . WriteFilesAsync ( uploadDir ) ;
2023-12-13 09:45:27 -05:00
await writer . FlushAsync ( ) ;
2024-01-16 18:08:13 -05:00
await channel . UploadFilesAsync ( "" , sandbox . GetLocator ( ) , storage . Backend ) ;
2023-04-11 15:00:24 -04:00
}
// Run the task remotely in the background and echo the output to the console
2023-07-28 11:51:34 -04:00
using ComputeChannel childProcessChannel = lease . Socket . CreateChannel ( ChildProcessChannelId ) ;
2023-08-21 14:38:59 -04:00
await using BackgroundTask tickTask = BackgroundTask . StartNew ( ctx = > WriteNumbersAsync ( childProcessChannel , lease . Socket . Logger , ctx ) ) ;
2023-07-28 11:51:34 -04:00
2023-08-11 11:25:26 -04:00
await using ( AgentManagedProcess process = await channel . ExecuteAsync ( executable , arguments , null , null , ExecuteProcessFlags . None ) )
2023-04-11 15:00:24 -04:00
{
2023-04-24 15:21:04 -04:00
string? line ;
2023-04-11 15:00:24 -04:00
while ( ( line = await process . ReadLineAsync ( ) ) ! = null )
{
2023-08-21 14:38:59 -04:00
lease . Socket . Logger . LogInformation ( "{Line}" , line ) ;
2023-04-11 15:00:24 -04:00
}
2023-03-29 13:03:35 -04:00
}
}
2023-04-11 15:00:24 -04:00
await lease . CloseAsync ( ) ;
2023-03-29 13:03:35 -04:00
}
2023-04-20 15:15:34 -04:00
2023-07-28 11:51:34 -04:00
static async Task WriteNumbersAsync ( ComputeChannel channel , ILogger logger , CancellationToken cancellationToken )
2023-03-29 13:03:35 -04:00
{
2023-05-20 14:50:49 -04:00
// Wait until the remote sends a message indicating that it's ready
2023-07-28 11:51:34 -04:00
if ( ! await channel . Reader . WaitToReadAsync ( 1 , cancellationToken ) )
2023-05-20 14:50:49 -04:00
{
2023-07-28 11:51:34 -04:00
throw new NotImplementedException ( ) ;
2023-05-20 14:50:49 -04:00
}
2023-04-20 16:15:10 -04:00
// Write data to the child process channel. The remote server will echo them back to us as it receives them, then exit when the channel is complete/closed.
2023-04-10 14:41:16 -04:00
byte [ ] buffer = new byte [ 4 ] ;
2023-04-10 19:30:39 -04:00
for ( int idx = 0 ; idx < 3 ; idx + + )
2023-03-24 12:41:04 -04:00
{
2023-04-10 14:41:16 -04:00
cancellationToken . ThrowIfCancellationRequested ( ) ;
logger . LogInformation ( "Writing value: {Value}" , idx ) ;
BinaryPrimitives . WriteInt32LittleEndian ( buffer , idx ) ;
2023-07-28 11:51:34 -04:00
await channel . Writer . WriteAsync ( buffer , cancellationToken ) ;
2023-04-10 14:41:16 -04:00
await Task . Delay ( 1000 , cancellationToken ) ;
2023-03-24 12:41:04 -04:00
}
2023-04-10 19:30:39 -04:00
2023-07-28 11:51:34 -04:00
channel . MarkComplete ( ) ;
2023-03-24 10:32:39 -04:00
}
2023-05-20 15:36:19 -04:00
static async Task RunBackgroundXorAsync ( AgentMessageChannel channel )
2023-04-20 15:15:34 -04:00
{
await channel . WaitForAttachAsync ( ) ;
byte [ ] dataToXor = new byte [ ] { 1 , 2 , 3 , 4 , 5 } ;
await channel . SendXorRequestAsync ( dataToXor , 123 ) ;
2023-05-21 09:27:26 -04:00
using AgentMessage response = await channel . ReceiveAsync ( AgentMessageType . XorResponse ) ;
2023-04-20 15:15:34 -04:00
for ( int idx = 0 ; idx < dataToXor . Length ; idx + + )
{
if ( response . Data . Span [ idx ] ! = ( byte ) ( dataToXor [ idx ] ^ 123 ) )
{
throw new InvalidOperationException ( ) ;
}
}
await channel . CloseAsync ( ) ;
}
2023-03-24 10:32:39 -04:00
}
}