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 ;
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 ) ;
}
}
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.
const int ControlChannelId = 0 ;
2023-04-11 15:00:24 -04:00
using ( IComputeMessageChannel channel = lease . Socket . CreateMessageChannel ( ControlChannelId , 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-04-11 15:00:24 -04:00
// Upload the sandbox
MemoryStorageClient storage = new MemoryStorageClient ( ) ;
using ( TreeWriter treeWriter = new TreeWriter ( storage ) )
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 ) ;
NodeHandle handle = await treeWriter . FlushAsync ( sandbox ) ;
await channel . UploadFilesAsync ( "" , handle . Locator , storage ) ;
}
// Run the task remotely in the background and echo the output to the console
await using ( IComputeProcess process = await channel . ExecuteAsync ( executable , arguments , null , null ) )
{
string? line = await process . ReadLineAsync ( ) ;
2023-03-29 13:03:35 -04:00
logger . LogInformation ( "[REMOTE] {Line}" , line ) ;
2023-04-11 15:00:24 -04:00
await using BackgroundTask tickTask = BackgroundTask . StartNew ( ctx = > WriteNumbersAsync ( lease . Socket , logger , ctx ) ) ;
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
}
static async Task WriteNumbersAsync ( IComputeSocket socket , ILogger logger , CancellationToken cancellationToken )
{
2023-03-24 12:41:04 -04:00
// Generate data into a buffer attached to channel 1. The remote server will echo them back to us as it receives them, then exit when the channel is complete/closed.
const int DataChannelId = 1 ;
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 ) ;
await socket . SendAsync ( DataChannelId , buffer , cancellationToken ) ;
await Task . Delay ( 1000 , cancellationToken ) ;
2023-03-24 12:41:04 -04:00
}
2023-04-10 19:30:39 -04:00
await socket . MarkCompleteAsync ( DataChannelId , cancellationToken ) ;
2023-03-24 10:32:39 -04:00
}
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
}
}