using System; using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; using Autofac; using Autofac.Extensions.DependencyInjection; using JetBrains.Annotations; using Kyoo.Abstractions.Controllers; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting.Systemd; using Microsoft.Extensions.Logging; using Microsoft.Win32; using Serilog; using Serilog.Templates; using Serilog.Templates.Themes; using ILogger = Serilog.ILogger; namespace Kyoo.Core { /// /// The main implementation of . /// Hosts of kyoo (main functions) generally only create a new /// and return . /// public class Application : IApplication { /// /// The environment in witch Kyoo will run (ether "Production" or "Development"). /// private readonly string _environment; /// /// The path to the data directory. /// private string _dataDir; /// /// Should the application restart after a shutdown? /// private bool _shouldRestart; /// /// The cancellation token source used to allow the app to be shutdown or restarted. /// private CancellationTokenSource _tokenSource; /// /// The logger used for startup and error messages. /// private ILogger _logger; /// /// Create a new that will use the specified environment. /// /// The environment to run in. public Application(string environment) { _environment = environment; } /// /// Start the application with the given console args. /// This is generally called from the Main entrypoint of Kyoo. /// /// The console arguments to use for kyoo. /// A task representing the whole process public Task Start(string[] args) { return Start(args, _ => { }); } /// /// Start the application with the given console args. /// This is generally called from the Main entrypoint of Kyoo. /// /// The console arguments to use for kyoo. /// A custom action to configure the container before the start /// A task representing the whole process public async Task Start(string[] args, Action configure) { _dataDir = _SetupDataDir(args); LoggerConfiguration config = new(); _ConfigureLogging(config, null, null); Log.Logger = config.CreateBootstrapLogger(); _logger = Log.Logger.ForContext(); AppDomain.CurrentDomain.ProcessExit += (_, _) => Log.CloseAndFlush(); AppDomain.CurrentDomain.UnhandledException += (_, ex) => Log.Fatal(ex.ExceptionObject as Exception, "Unhandled exception"); do { IHost host = _CreateWebHostBuilder(args) .ConfigureContainer(configure) .Build(); _tokenSource = new CancellationTokenSource(); await _StartWithHost(host, _tokenSource.Token); } while (_shouldRestart); } /// public void Shutdown() { _shouldRestart = false; _tokenSource.Cancel(); } /// public void Restart() { _shouldRestart = true; _tokenSource.Cancel(); } /// public string GetDataDirectory() { return _dataDir; } /// public string GetConfigFile() { return "./settings.json"; } /// /// Parse the data directory from environment variables and command line arguments, create it if necessary. /// Set the current directory to said data folder and place a default configuration file if it does not already /// exists. /// /// The command line arguments /// The current data directory. private string _SetupDataDir(string[] args) { Dictionary registry = new(); if (OperatingSystem.IsWindows()) { object dataDir = Registry.GetValue(@"HKEY_LOCAL_MACHINE\Software\SDG\Kyoo\Settings", "DataDir", null) ?? Registry.GetValue(@"HKEY_CURRENT_USER\Software\SDG\Kyoo\Settings", "DataDir", null); if (dataDir is string data) registry.Add("DataDir", data); } IConfiguration parsed = new ConfigurationBuilder() .AddInMemoryCollection(registry) .AddEnvironmentVariables() .AddEnvironmentVariables("KYOO_") .AddCommandLine(args) .Build(); string path = parsed.GetValue("datadir"); path ??= Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Kyoo"); path = Path.GetFullPath(path); if (!Directory.Exists(path)) Directory.CreateDirectory(path); Environment.CurrentDirectory = path; if (!File.Exists(GetConfigFile())) { File.Copy(Path.Join(AppDomain.CurrentDomain.BaseDirectory, GetConfigFile()), GetConfigFile()); } return path; } /// /// Start the given host and log failing exceptions. /// /// The host to start. /// A token to allow one to stop the host. private async Task _StartWithHost(IHost host, CancellationToken cancellationToken) { try { _logger.Information("Running as {Name}", Environment.UserName); _logger.Information("Data directory: {DataDirectory}", GetDataDirectory()); await host.RunAsync(cancellationToken); } catch (Exception ex) { _logger.Fatal(ex, "Unhandled exception"); } } /// /// Create a a web host /// /// Command line parameters that can be handled by kestrel /// A new web host instance private IHostBuilder _CreateWebHostBuilder(string[] args) { IConfiguration configuration = _SetupConfig(new ConfigurationBuilder(), args).Build(); return new HostBuilder() .UseServiceProviderFactory(new AutofacServiceProviderFactory()) .UseContentRoot(AppDomain.CurrentDomain.BaseDirectory) .UseEnvironment(_environment) .UseSystemd() .ConfigureAppConfiguration(x => _SetupConfig(x, args)) .UseSerilog((host, services, builder) => _ConfigureLogging(builder, host.Configuration, services)) .ConfigureServices(x => x.AddRouting()) .ConfigureContainer(x => { x.RegisterInstance(this).As().SingleInstance().ExternallyOwned(); }) .ConfigureWebHost(x => x .UseKestrel(options => { options.AddServerHeader = false; }) .UseIIS() .UseIISIntegration() .UseUrls(configuration.GetValue("basics:url")) .UseStartup(host => PluginsStartup.FromWebHost(host, new LoggerFactory().AddSerilog())) ); } /// /// Register settings.json, environment variables and command lines arguments as configuration. /// /// The configuration builder to use /// The command line arguments /// The modified configuration builder private IConfigurationBuilder _SetupConfig(IConfigurationBuilder builder, string[] args) { return builder.SetBasePath(GetDataDirectory()) .AddJsonFile(Path.Join(AppDomain.CurrentDomain.BaseDirectory, GetConfigFile()), false, true) .AddJsonFile(GetConfigFile(), false, true) .AddEnvironmentVariables() .AddEnvironmentVariables("KYOO_") .AddCommandLine(args); } /// /// Configure the logging. /// /// The logger builder to configure. /// The configuration to read settings from. /// The services to read configuration from. private void _ConfigureLogging(LoggerConfiguration builder, [CanBeNull] IConfiguration configuration, [CanBeNull] IServiceProvider services) { if (configuration != null) { try { builder.ReadFrom.Configuration(configuration, "logging"); } catch (Exception ex) { _logger.Fatal(ex, "Could not read serilog configuration"); } } if (services != null) builder.ReadFrom.Services(services); const string template = "[{@t:HH:mm:ss} {@l:u3} {Substring(SourceContext, LastIndexOf(SourceContext, '.') + 1), 15} " + "({@i:D10})] {@m}{#if not EndsWith(@m, '\n')}\n{#end}{@x}"; if (SystemdHelpers.IsSystemdService()) { const string syslogTemplate = "[{SourceContext,-35}] {Message:lj}{NewLine}{Exception}"; builder .WriteTo.Console(new ExpressionTemplate(template, theme: TemplateTheme.Code)) .WriteTo.LocalSyslog("Kyoo", outputTemplate: syslogTemplate) .Enrich.WithThreadId() .Enrich.FromLogContext(); return; } builder .WriteTo.Console(new ExpressionTemplate(template, theme: TemplateTheme.Code)) .WriteTo.File( path: Path.Combine(GetDataDirectory(), "logs", "log-.log"), formatter: new ExpressionTemplate(template), rollingInterval: RollingInterval.Day, rollOnFileSizeLimit: true ) .Enrich.WithThreadId() .Enrich.FromLogContext(); } } }