// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.InteropServices; using System.Threading.Tasks; using EpicGames.Core; using Horde.Build.Commands; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using OpenTracing; using OpenTracing.Propagation; using OpenTracing.Util; using Serilog; using Serilog.Configuration; using Serilog.Core; using Serilog.Events; using Serilog.Formatting.Json; using Serilog.Sinks.SystemConsole.Themes; namespace Horde.Build { static class LoggerExtensions { public static LoggerConfiguration Console(this LoggerSinkConfiguration sinkConfig, ServerSettings settings) { if (settings.LogJsonToStdOut) { return sinkConfig.Console(new JsonFormatter(renderMessage: true)); } else { ConsoleTheme theme; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && Environment.OSVersion.Version < new Version(10, 0)) { theme = SystemConsoleTheme.Literate; } else { theme = AnsiConsoleTheme.Code; } return sinkConfig.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:w3}] {Indent}{Message:l}{NewLine}{Exception}", theme: theme, restrictedToMinimumLevel: LogEventLevel.Debug); } } public static LoggerConfiguration WithHordeConfig(this LoggerConfiguration configuration, ServerSettings settings) { if (settings.WithDatadog) { configuration = configuration.Enrich.With(); } return configuration; } } class DatadogLogEnricher : ILogEventEnricher { public void Enrich(Serilog.Events.LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { ISpan? span = GlobalTracer.Instance?.ActiveSpan; if (span != null) { logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("dd.trace_id", span.Context.TraceId)); logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("dd.span_id", span.Context.SpanId)); } } } class TestTracer : ITracer { readonly ITracer _inner; public TestTracer(ITracer inner) { _inner = inner; } public IScopeManager ScopeManager => _inner.ScopeManager; public ISpan ActiveSpan => _inner.ActiveSpan; public ISpanBuilder BuildSpan(string operationName) { Serilog.Log.Debug("Creating span {Name}", operationName); return _inner.BuildSpan(operationName); } public ISpanContext Extract(IFormat format, TCarrier carrier) { return _inner.Extract(format, carrier); } public void Inject(ISpanContext spanContext, IFormat format, TCarrier carrier) { _inner.Inject(spanContext, format, carrier); } } class Program { public static SemVer Version => _version; public static DirectoryReference AppDir { get; } = GetAppDir(); public static DirectoryReference DataDir { get; } = GetDefaultDataDir(); public static FileReference UserConfigFile { get; } = FileReference.Combine(GetDefaultDataDir(), "Horde.json"); public static Type[] ConfigSchemas = FindSchemaTypes(); static SemVer _version; static Type[] FindSchemaTypes() { List schemaTypes = new List(); foreach (Type type in Assembly.GetExecutingAssembly().GetTypes()) { if (type.GetCustomAttribute() != null) { schemaTypes.Add(type); } } return schemaTypes.ToArray(); } public static async Task Main(string[] args) { FileVersionInfo versionInfo = FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location); if (String.IsNullOrEmpty(versionInfo.ProductVersion)) { _version = SemVer.Parse("0.0.0"); } else { _version = SemVer.Parse(versionInfo.ProductVersion); } CommandLineArguments arguments = new CommandLineArguments(args); IConfiguration config = new ConfigurationBuilder() .SetBasePath(AppDir.FullName) .AddJsonFile("appsettings.json", optional: false) .AddJsonFile("appsettings.Build.json", optional: true) // specific settings for builds (installer/dockerfile) .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json", optional: true) // environment variable overrides, also used in k8s setups with Helm .AddJsonFile("appsettings.User.json", optional: true) .AddJsonFile(UserConfigFile.FullName, optional: true, reloadOnChange: true) .AddEnvironmentVariables() .Build(); ServerSettings hordeSettings = new ServerSettings(); config.GetSection("Horde").Bind(hordeSettings); InitializeDefaults(hordeSettings); DirectoryReference logDir = AppDir; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { logDir = DirectoryReference.Combine(DataDir); } Serilog.Log.Logger = new LoggerConfiguration() .WithHordeConfig(hordeSettings) .Enrich.FromLogContext() .WriteTo.Console(hordeSettings) .WriteTo.File(Path.Combine(logDir.FullName, "Log.txt"), outputTemplate: "[{Timestamp:HH:mm:ss} {Level:w3}] {Indent}{Message:l}{NewLine}{Exception} [{SourceContext}]", rollingInterval: RollingInterval.Day, rollOnFileSizeLimit: true, fileSizeLimitBytes: 20 * 1024 * 1024, retainedFileCountLimit: 10) .WriteTo.File(new JsonFormatter(renderMessage: true), Path.Combine(logDir.FullName, "Log.json"), rollingInterval: RollingInterval.Day, rollOnFileSizeLimit: true, fileSizeLimitBytes: 20 * 1024 * 1024, retainedFileCountLimit: 10) .ReadFrom.Configuration(config) .CreateLogger(); Serilog.Log.Logger.Information("Server version: {Version}", Version); if (hordeSettings.WithDatadog) { GlobalTracer.Register(Datadog.Trace.OpenTracing.OpenTracingTracerFactory.WrapTracer(Datadog.Trace.Tracer.Instance)); Serilog.Log.Logger.Information("Enabling datadog tracing (OpenTrace)"); } IServiceCollection services = new ServiceCollection(); services.AddCommandsFromAssembly(Assembly.GetExecutingAssembly()); services.AddLogging(builder => builder.AddSerilog()); services.AddSingleton(config); services.AddSingleton(hordeSettings); #pragma warning disable ASP0000 // Do not call 'IServiceCollection.BuildServiceProvider' in 'ConfigureServices' IServiceProvider serviceProvider = services.BuildServiceProvider(); return await CommandHost.RunAsync(arguments, serviceProvider, typeof(ServerCommand)); #pragma warning restore ASP0000 // Do not call 'IServiceCollection.BuildServiceProvider' in 'ConfigureServices' } // Used by WebApplicationFactory in controller tests. Uses reflection to call this exact function signature. public static IHostBuilder CreateHostBuilder(string[] args) => ServerCommand.CreateHostBuilderForTesting(args); /// /// Get the application directory /// /// static DirectoryReference GetAppDir() { return new FileReference(Assembly.GetExecutingAssembly().Location).Directory; } /// /// Gets the default directory for storing application data /// /// The default data directory static DirectoryReference GetDefaultDataDir() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { DirectoryReference? dir = DirectoryReference.GetSpecialFolder(Environment.SpecialFolder.CommonApplicationData); if (dir != null) { return DirectoryReference.Combine(dir, "HordeServer"); } } return DirectoryReference.Combine(GetAppDir(), "Data"); } /// /// Handles bootstrapping of defaults for local servers, which can't be generated during build/installation process (or are better handled here where they can be updated) /// This stuff will change as we get settings into database and could be considered discovery for installer/dockerfile builds /// static void InitializeDefaults(ServerSettings settings) { if (settings.SingleInstance) { FileReference globalConfig = FileReference.Combine(Program.DataDir, "Config/globals.json"); if (!FileReference.Exists(globalConfig)) { DirectoryReference.CreateDirectory(globalConfig.Directory); FileReference.WriteAllText(globalConfig, "{}"); } FileReference privateCertFile = FileReference.Combine(Program.DataDir, "Agent/ServerToAgent.pfx"); string privateCertFileJsonPath = privateCertFile.ToString().Replace("\\", "/", StringComparison.Ordinal); if (!FileReference.Exists(UserConfigFile)) { // create new user configuration DirectoryReference.CreateDirectory(UserConfigFile.Directory); FileReference.WriteAllText(UserConfigFile, $"{{\"Horde\": {{ \"ConfigPath\" : \"{globalConfig.ToString().Replace("\\", "/", StringComparison.Ordinal)}\", \"ServerPrivateCert\" : \"{privateCertFileJsonPath}\", \"HttpPort\": 8080}}}}"); } // make sure the cert exists if (!FileReference.Exists(privateCertFile)) { string dnsName = System.Net.Dns.GetHostName(); Serilog.Log.Logger.Information("Creating certificate for {DnsName}", dnsName); byte[] privateCertData = CertificateUtils.CreateSelfSignedCert(dnsName, "Horde Server"); Serilog.Log.Logger.Information("Writing private cert: {PrivateCert}", privateCertFile.FullName); if (!DirectoryReference.Exists(privateCertFile.Directory)) { DirectoryReference.CreateDirectory(privateCertFile.Directory); } FileReference.WriteAllBytes(privateCertFile, privateCertData); } // note: this isn't great, though we need it early in server startup, and this is only hit on first server boot where the grpc cert isn't generated/set if (settings.ServerPrivateCert == null) { settings.ServerPrivateCert = privateCertFile.ToString(); } } } } }