2022-02-15 17:20:45 -05:00
// Copyright Epic Games, Inc. All Rights Reserved.
using System ;
using System.IO ;
2024-02-21 14:10:18 -05:00
using System.Linq ;
2024-02-21 22:32:38 -05:00
using System.Runtime.Versioning ;
2022-02-15 17:20:45 -05:00
using System.Security.Cryptography.X509Certificates ;
2024-02-21 14:10:18 -05:00
using System.Threading ;
2022-02-15 17:20:45 -05:00
using System.Threading.Tasks ;
2022-03-23 14:50:23 -04:00
using EpicGames.Core ;
using Microsoft.AspNetCore.Hosting ;
using Microsoft.AspNetCore.Server.Kestrel.Core ;
using Microsoft.Extensions.Configuration ;
2024-02-21 14:10:18 -05:00
using Microsoft.Extensions.DependencyInjection ;
2022-03-23 14:50:23 -04:00
using Microsoft.Extensions.Hosting ;
2024-02-21 14:10:18 -05:00
using Microsoft.Extensions.Hosting.WindowsServices ;
2022-03-23 14:50:23 -04:00
using Microsoft.Extensions.Logging ;
2024-02-21 14:10:18 -05:00
using Microsoft.Extensions.Options ;
2022-03-23 14:50:23 -04:00
using Serilog ;
2022-02-15 17:20:45 -05:00
2023-03-17 09:50:40 -04:00
namespace Horde.Server.Commands
2022-02-15 17:20:45 -05:00
{
using ILogger = Microsoft . Extensions . Logging . ILogger ;
[Command("server", "Runs the Horde Build server (default)")]
class ServerCommand : Command
{
2022-03-23 14:50:23 -04:00
readonly ServerSettings _hordeSettings ;
readonly IConfiguration _config ;
string [ ] _args = Array . Empty < string > ( ) ;
2022-02-15 17:20:45 -05:00
2022-03-23 14:50:23 -04:00
public ServerCommand ( ServerSettings settings , IConfiguration config )
2022-02-15 17:20:45 -05:00
{
2022-03-23 14:50:23 -04:00
_hordeSettings = settings ;
_config = config ;
2022-02-15 17:20:45 -05:00
}
2022-03-23 14:50:23 -04:00
public override void Configure ( CommandLineArguments arguments , ILogger logger )
2022-02-15 17:20:45 -05:00
{
2022-03-23 14:50:23 -04:00
base . Configure ( arguments , logger ) ;
_args = arguments . GetRawArray ( ) ;
2022-02-15 17:20:45 -05:00
}
2022-03-23 14:50:23 -04:00
public override async Task < int > ExecuteAsync ( ILogger logger )
2022-02-15 17:20:45 -05:00
{
2023-12-01 13:34:28 -05:00
logger . LogInformation ( "Server version: {Version}" , ServerApp . Version ) ;
Horde: Config and directory layout changes.
Note: Logs and artifacts produced by local builds will be under the application output folder by default after this change (ie. under bin/Debug/net8.0/Data), rather than in C:\ProgramData.
* The ServerSettings.Installed flag now controls whether the server runs in "installed" mode or not. This setting defaults to true, but is overridden to false in appsettings.Local.json to make it easy to launch the server in an IDE (the default launchSettings.json sets ASPNETCORE_ENVIRONMENT to "Local"). The Windows installer build process excludes the appsettings.Local.json file, letting it fall back to its default value.
* The user "data" directory, where logs, artifacts, and so on are kept, is moved to the "Data" folder underneath the directory containing the application. In installed mode on Windows, the data directory is set to C:\ProgramData\Epic\Horde\Server instead. In either case, the data directory can be overridden using the ServerSettings.DataDir property.
* The user "config" directory, where configuration files are read from, is the same as the "data" directory in installed builds. Default user configuration files are copied from the "Defaults" folder to the "data" directory on startup if not present, allowing users to modify them without requiring elevated permissions. For non-installed builds, the default configuration files are always read directly from the "Defaults" folder, preventing stale configuration data on the system pollute a debugging session.
* A new server.json in the user config directory allows modifying the same settings as appsettings.json.
[FYI] Josh.Engebretson, Carl.Bystrom
#jira UE-205418
[CL 31109803 by ben marsh in ue5-main branch]
2024-02-01 18:41:12 -05:00
logger . LogInformation ( "App directory: {AppDir}" , ServerApp . AppDir ) ;
logger . LogInformation ( "Data directory: {DataDir}" , ServerApp . DataDir ) ;
logger . LogInformation ( "Server config: {ConfigFile}" , ServerApp . ServerConfigFile ) ;
2023-12-01 13:34:28 -05:00
2022-03-23 14:50:23 -04:00
using ( X509Certificate2 ? grpcCertificate = ReadGrpcCertificate ( _hordeSettings ) )
2022-02-15 17:20:45 -05:00
{
2022-08-23 18:52:06 -04:00
using IHost host = CreateHostBuilderWithCert ( _args , _config , _hordeSettings , grpcCertificate ) . Build ( ) ;
2023-05-05 09:23:55 -04:00
2022-08-23 18:52:06 -04:00
await host . RunAsync ( ) ;
2022-02-15 17:20:45 -05:00
return 0 ;
}
}
2022-03-23 14:50:23 -04:00
static IHostBuilder CreateHostBuilderWithCert ( string [ ] args , IConfiguration config , ServerSettings serverSettings , X509Certificate2 ? sslCert )
2022-02-15 17:20:45 -05:00
{
2023-05-08 20:12:47 -04:00
AppContext . SetSwitch ( "System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport" , true ) ;
2023-05-05 09:23:55 -04:00
IHostBuilder hostBuilder = Host . CreateDefaultBuilder ( args )
2022-02-15 17:20:45 -05:00
. UseSerilog ( )
2022-03-23 14:50:23 -04:00
. ConfigureAppConfiguration ( builder = > builder . AddConfiguration ( config ) )
. ConfigureWebHostDefaults ( webBuilder = >
2022-02-15 17:20:45 -05:00
{
2023-05-08 20:12:47 -04:00
webBuilder . UseUrls ( ) ; // Disable default URLs; we will configure each port directly.
2024-01-26 17:15:20 -05:00
webBuilder . UseWebRoot ( "DashboardApp" ) ;
2022-03-23 14:50:23 -04:00
webBuilder . ConfigureKestrel ( options = >
2022-02-15 17:20:45 -05:00
{
2023-05-18 15:47:34 -04:00
options . Limits . MaxRequestBodySize = 256 * 1024 * 1024 ;
2024-02-21 14:10:18 -05:00
2023-05-10 08:38:06 -04:00
// When agents are saturated with work (CPU or I/O), slow sending of gRPC data can happen.
// Kestrel protects against this behavior by default as it's commonly used for malicious attacks.
// Setting a more generous data rate should prevent incoming HTTP connections from being closed prematurely.
options . Limits . MinRequestBodyDataRate = new MinDataRate ( 10 , TimeSpan . FromSeconds ( 60 ) ) ;
options . Limits . KeepAliveTimeout = TimeSpan . FromSeconds ( 220 ) ; // 10 seconds more than agent's timeout
2022-02-15 17:20:45 -05:00
2024-01-18 13:49:50 -05:00
int httpPort = serverSettings . HttpPort ;
2023-12-18 09:20:51 -05:00
if ( httpPort ! = 0 )
{
2024-01-26 17:15:20 -05:00
options . ListenAnyIP ( httpPort , configure = > { configure . Protocols = HttpProtocols . Http1 ; } ) ;
2022-02-15 17:20:45 -05:00
}
2024-01-18 13:49:50 -05:00
int httpsPort = serverSettings . HttpsPort ;
if ( httpsPort ! = 0 )
2022-02-15 17:20:45 -05:00
{
2024-02-21 14:10:18 -05:00
options . ListenAnyIP ( httpsPort , configure = >
2023-05-08 20:12:47 -04:00
{
if ( sslCert ! = null )
{
configure . UseHttps ( sslCert ) ;
}
else
{
configure . UseHttps ( ) ;
}
2022-03-23 14:50:23 -04:00
} ) ;
2022-02-15 17:20:45 -05:00
}
// To serve HTTP/2 with gRPC *without* TLS enabled, a separate port for HTTP/2 must be used.
// This is useful when having a load balancer in front that terminates TLS.
2024-01-18 13:49:50 -05:00
int http2Port = serverSettings . Http2Port ;
2023-12-18 09:20:51 -05:00
if ( http2Port ! = 0 )
{
2024-01-18 13:49:50 -05:00
options . ListenAnyIP ( http2Port , configure = > { configure . Protocols = HttpProtocols . Http2 ; } ) ;
2022-02-15 17:20:45 -05:00
}
} ) ;
2022-03-23 14:50:23 -04:00
webBuilder . UseStartup < Startup > ( ) ;
2022-02-15 17:20:45 -05:00
} ) ;
2023-05-05 09:23:55 -04:00
2024-02-21 22:32:38 -05:00
if ( OperatingSystem . IsWindows ( ) & & WindowsServiceHelpers . IsWindowsService ( ) )
2023-05-05 09:23:55 -04:00
{
// Attempt to setup this process as a Windows service. A race condition inside Microsoft.Extensions.Hosting.WindowsServices.WindowsServiceHelpers.IsWindowsService
// can result in accessing the parent process after it's terminated, so catch any exceptions that it throws.
try
{
2024-02-21 14:10:18 -05:00
// Register the default WindowsServiceLifetime
2023-05-05 09:23:55 -04:00
hostBuilder = hostBuilder . UseWindowsService ( ) ;
2024-02-21 14:10:18 -05:00
2024-02-21 22:32:38 -05:00
#pragma warning disable CA1416
2024-02-21 14:10:18 -05:00
// Replace the default WindowsServiceLifetime (if there is one; we may not be running as a service) with a custom one
// that waits for all application startup before the service enters the running state. See https://github.com/dotnet/runtime/issues/50019
hostBuilder = hostBuilder . ConfigureServices ( services = >
{
ServiceDescriptor descriptor = services . First ( x = > x . ImplementationType = = typeof ( WindowsServiceLifetime ) ) ;
services . Remove ( descriptor ) ;
services . AddSingleton < IHostLifetime , CustomWindowsServiceLifetime > ( ) ;
} ) ;
2024-02-21 22:32:38 -05:00
#pragma warning restore CA1416
2023-05-05 09:23:55 -04:00
}
catch ( InvalidOperationException )
{
}
}
return hostBuilder ;
2022-02-15 17:20:45 -05:00
}
2024-02-21 14:10:18 -05:00
// Custom service lifetime to wait for startup before continuing
2024-02-21 22:32:38 -05:00
[SupportedOSPlatform("windows")]
2024-02-21 14:10:18 -05:00
sealed class CustomWindowsServiceLifetime : WindowsServiceLifetime , IHostLifetime
{
readonly IHostApplicationLifetime _applicationLifetime ;
readonly ManualResetEventSlim _applicationStarted = new ManualResetEventSlim ( false ) ;
2024-02-21 22:32:38 -05:00
readonly TaskCompletionSource _serviceStarting = new TaskCompletionSource ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
2024-02-21 14:10:18 -05:00
readonly ILogger _logger ;
public CustomWindowsServiceLifetime ( IHostEnvironment environment , IHostApplicationLifetime applicationLifetime , ILoggerFactory loggerFactory , IOptions < HostOptions > optionsAccessor )
: base ( environment , applicationLifetime , loggerFactory , optionsAccessor )
{
_applicationLifetime = applicationLifetime ;
_applicationLifetime . ApplicationStarted . Register ( ( ) = > _applicationStarted . Set ( ) ) ;
_logger = loggerFactory . CreateLogger < CustomWindowsServiceLifetime > ( ) ;
}
protected override void Dispose ( bool disposing )
{
if ( disposing )
{
_applicationStarted . Dispose ( ) ;
}
base . Dispose ( disposing ) ;
}
public new async Task WaitForStartAsync ( CancellationToken cancellationToken )
{
Task baseStartTask = base . WaitForStartAsync ( cancellationToken ) ;
await await Task . WhenAny ( baseStartTask , _serviceStarting . Task ) ;
}
protected override void OnStart ( string [ ] args )
{
// Win32 service is starting; need to synchronously wait until application has finished startup.
_logger . LogInformation ( "Win32 service starting..." ) ;
_serviceStarting . TrySetResult ( ) ;
_applicationStarted . Wait ( ) ;
base . OnStart ( args ) ;
_logger . LogInformation ( "Win32 service startup complete." ) ;
}
}
2022-02-15 17:20:45 -05:00
/// <summary>
/// Gets the certificate to use for Grpc endpoints
/// </summary>
/// <returns>Custom certificate to use for Grpc endpoints, or null for the default.</returns>
2022-03-23 14:50:23 -04:00
public static X509Certificate2 ? ReadGrpcCertificate ( ServerSettings hordeSettings )
2022-02-15 17:20:45 -05:00
{
2022-03-23 14:50:23 -04:00
string base64Prefix = "base64:" ;
2022-02-15 17:20:45 -05:00
2022-03-23 14:50:23 -04:00
if ( hordeSettings . ServerPrivateCert = = null )
2022-02-15 17:20:45 -05:00
{
return null ;
}
2022-03-23 14:50:23 -04:00
else if ( hordeSettings . ServerPrivateCert . StartsWith ( base64Prefix , StringComparison . Ordinal ) )
2022-02-15 17:20:45 -05:00
{
2022-03-23 14:50:23 -04:00
byte [ ] certData = Convert . FromBase64String ( hordeSettings . ServerPrivateCert . Replace ( base64Prefix , "" , StringComparison . Ordinal ) ) ;
return new X509Certificate2 ( certData ) ;
2022-02-15 17:20:45 -05:00
}
else
{
2022-03-23 14:50:23 -04:00
FileReference ? serverPrivateCert ;
if ( ! Path . IsPathRooted ( hordeSettings . ServerPrivateCert ) )
2022-02-15 17:20:45 -05:00
{
2023-09-18 21:19:48 -04:00
serverPrivateCert = FileReference . Combine ( ServerApp . AppDir , hordeSettings . ServerPrivateCert ) ;
2022-02-15 17:20:45 -05:00
}
else
{
2022-03-23 14:50:23 -04:00
serverPrivateCert = new FileReference ( hordeSettings . ServerPrivateCert ) ;
2022-02-15 17:20:45 -05:00
}
2022-03-23 14:50:23 -04:00
return new X509Certificate2 ( FileReference . ReadAllBytes ( serverPrivateCert ) ) ;
2022-02-15 17:20:45 -05:00
}
}
2022-02-28 11:09:00 -05:00
2022-03-23 14:50:23 -04:00
public static IHostBuilder CreateHostBuilderForTesting ( string [ ] args )
2022-02-28 11:09:00 -05:00
{
2022-03-23 14:50:23 -04:00
ServerSettings hordeSettings = new ServerSettings ( ) ;
return CreateHostBuilderWithCert ( args , new ConfigurationBuilder ( ) . Build ( ) , hordeSettings , null ) ;
2022-02-28 11:09:00 -05:00
}
2024-02-21 14:10:18 -05:00
}
2022-02-15 17:20:45 -05:00
}