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-29 13:03:35 -04:00
using System.Net.Http.Headers ;
2023-03-24 10:32:39 -04:00
using System.Reflection ;
using EpicGames.Core ;
2023-03-29 13:03:35 -04:00
using EpicGames.Horde.Common ;
2023-03-24 10:32:39 -04:00
using EpicGames.Horde.Compute ;
2023-05-20 14:50:49 -04:00
using EpicGames.Horde.Compute.Buffers ;
2023-03-24 10:32:39 -04:00
using EpicGames.Horde.Compute.Clients ;
using EpicGames.Horde.Storage ;
using EpicGames.Horde.Storage.Backends ;
using EpicGames.Horde.Storage.Nodes ;
2023-03-24 18:13:10 -04:00
using EpicGames.OIDC ;
using Microsoft.Extensions.Configuration ;
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
[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-03-24 12:41:04 -04:00
// Create the client to handle our requests
2023-03-29 13:03:35 -04:00
await using IComputeClient client = await CreateClientAsync ( options , logger ) ;
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 ) ) ;
}
await using IComputeLease ? lease = await client . TryAssignWorkerAsync ( new ClusterId ( "default" ) , requirements ) ;
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" ) ;
await RunRemoteAsync ( lease , remoteServerFile . Directory , "RemoteWorkerCpp.exe" , new List < string > ( ) , logger ) ;
}
else
{
FileReference remoteServerFile = FileReference . Combine ( ClientSourceDir , "../RemoteWorker" , CurrentAssemblyFile . Directory . MakeRelativeTo ( ClientSourceDir ) , "RemoteWorker.dll" ) ;
await RunRemoteAsync ( lease , remoteServerFile . Directory , @"C:\Program Files\dotnet\dotnet.exe" , new List < string > { remoteServerFile . GetFileName ( ) } , logger ) ;
}
}
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-03-30 15:46:03 -04:00
static async Task RunRemoteAsync ( IComputeLease lease , DirectoryReference uploadDir , string executable , List < string > arguments , ILogger logger )
{
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-05-20 15:36:19 -04:00
using ( AgentMessageChannel channel = lease . Socket . CreateAgentMessageChannel ( PrimaryChannelId , 4 * 1024 * 1024 , logger ) )
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-05-20 15:36:19 -04:00
using AgentMessageChannel backgroundChannel = lease . Socket . CreateAgentMessageChannel ( BackgroundChannelId , 4 * 1024 * 1024 , logger ) ;
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.
2023-04-11 15:00:24 -04:00
MemoryStorageClient storage = new MemoryStorageClient ( ) ;
2023-06-07 22:20:37 -04:00
await using ( IStorageWriter treeWriter = storage . CreateWriter ( ) )
2023-03-29 13:03:35 -04:00
{
2023-04-11 15:00:24 -04:00
DirectoryNode sandbox = new DirectoryNode ( ) ;
await sandbox . CopyFromDirectoryAsync ( uploadDir . ToDirectoryInfo ( ) , new ChunkingOptions ( ) , treeWriter , null ) ;
2023-06-09 11:57:14 -04:00
BlobHandle handle = await treeWriter . FlushAsync ( sandbox ) ;
2023-06-07 13:19:54 -04:00
await channel . UploadFilesAsync ( "" , handle . GetLocator ( ) , storage ) ;
2023-04-11 15:00:24 -04:00
}
// Run the task remotely in the background and echo the output to the console
2023-05-20 15:36:19 -04:00
await using ( AgentManagedProcess process = await channel . ExecuteAsync ( executable , arguments , null , null ) )
2023-04-11 15:00:24 -04:00
{
await using BackgroundTask tickTask = BackgroundTask . StartNew ( ctx = > WriteNumbersAsync ( lease . Socket , logger , ctx ) ) ;
2023-04-24 15:21:04 -04:00
string? line ;
2023-04-11 15:00:24 -04:00
while ( ( line = await process . ReadLineAsync ( ) ) ! = null )
{
logger . LogInformation ( "[REMOTE] {Line}" , line ) ;
}
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-05-22 10:44:26 -04:00
static async Task WriteNumbersAsync ( ComputeSocket socket , 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
using ( PooledBuffer recvBuffer = new PooledBuffer ( 1 , 20 ) )
{
socket . AttachRecvBuffer ( ChildProcessChannelId , recvBuffer . Writer ) ;
await recvBuffer . Reader . WaitToReadAsync ( 1 , cancellationToken ) ;
}
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-04-20 16:15:10 -04:00
await socket . SendAsync ( ChildProcessChannelId , 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-04-20 16:15:10 -04:00
await socket . MarkCompleteAsync ( ChildProcessChannelId , cancellationToken ) ;
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-29 13:03:35 -04:00
static async Task < IComputeClient > CreateClientAsync ( ClientAppOptions options , ILogger logger )
2023-03-24 10:32:39 -04:00
{
2023-03-29 13:03:35 -04:00
if ( options . Server = = null )
2023-03-24 10:32:39 -04:00
{
2023-03-27 13:27:23 -04:00
DirectoryReference sandboxDir = DirectoryReference . Combine ( new FileReference ( Assembly . GetExecutingAssembly ( ) . Location ) . Directory , "Sandbox" ) ;
return new LocalComputeClient ( 2000 , sandboxDir , logger ) ;
2023-03-24 10:32:39 -04:00
}
else
{
2023-03-29 13:03:35 -04:00
AuthenticationHeaderValue ? authHeader = await GetAuthHeaderAsync ( options , logger ) ;
return new ServerComputeClient ( new Uri ( options . Server ) , authHeader , logger ) ;
2023-03-24 10:32:39 -04:00
}
}
2023-03-24 18:13:10 -04:00
2023-03-29 13:03:35 -04:00
static async Task < AuthenticationHeaderValue ? > GetAuthHeaderAsync ( ClientAppOptions options , ILogger logger )
2023-03-24 18:13:10 -04:00
{
2023-03-29 13:03:35 -04:00
if ( options . OidcProvider = = null )
{
return null ;
}
for ( DirectoryReference ? currentDir = CurrentAssemblyFile . Directory ; currentDir ! = null ; currentDir = currentDir . ParentDirectory )
{
FileReference buildVersionFile = FileReference . Combine ( currentDir , "Build/Build.version" ) ;
if ( FileReference . Exists ( buildVersionFile ) )
{
string bearerToken = await GetOidcBearerTokenAsync ( currentDir , null , options . OidcProvider , logger ) ;
return new AuthenticationHeaderValue ( "Bearer" , bearerToken ) ;
}
}
throw new Exception ( $"Unable to find engine directory above {CurrentAssemblyFile}" ) ;
}
static async Task < string > GetOidcBearerTokenAsync ( DirectoryReference engineDir , DirectoryReference ? projectDir , string oidcProvider , ILogger logger )
{
logger . LogInformation ( "Performing OIDC token refresh..." ) ;
2023-03-24 18:13:10 -04:00
using ITokenStore tokenStore = TokenStoreFactory . CreateTokenStore ( ) ;
IConfiguration providerConfiguration = ProviderConfigurationFactory . ReadConfiguration ( engineDir . ToDirectoryInfo ( ) , projectDir ? . ToDirectoryInfo ( ) ) ;
2023-03-29 13:03:35 -04:00
OidcTokenManager oidcTokenManager = OidcTokenManager . CreateTokenManager ( providerConfiguration , tokenStore , new List < string > ( ) { oidcProvider } ) ;
2023-04-11 16:41:59 -04:00
OidcTokenInfo result ;
try
{
result = await oidcTokenManager . GetAccessToken ( oidcProvider ) ;
}
catch ( NotLoggedInException )
{
result = await oidcTokenManager . Login ( oidcProvider ) ;
}
2023-03-24 18:13:10 -04:00
if ( result . AccessToken = = null )
{
2023-03-29 13:03:35 -04:00
throw new Exception ( $"Unable to get access token for {oidcProvider}" ) ;
2023-03-24 18:13:10 -04:00
}
2023-03-29 13:03:35 -04:00
logger . LogInformation ( "Received bearer token for {OidcProvider}" , oidcProvider ) ;
2023-03-24 18:13:10 -04:00
return result . AccessToken ;
}
2023-03-24 10:32:39 -04:00
}
}