2019-01-13 14:30:41 -05:00
using System ;
2020-06-15 11:06:57 -04:00
using System.Collections.Generic ;
2019-01-13 14:30:41 -05:00
using System.Diagnostics ;
using System.IO ;
using System.Linq ;
using System.Net ;
using System.Reflection ;
2019-10-26 16:53:53 -04:00
using System.Text ;
2019-01-12 17:58:58 -05:00
using System.Threading ;
2019-01-13 14:30:41 -05:00
using System.Threading.Tasks ;
2019-01-28 15:58:47 -05:00
using CommandLine ;
2019-01-13 14:30:41 -05:00
using Emby.Server.Implementations ;
using Emby.Server.Implementations.IO ;
2021-05-11 17:26:00 -04:00
using Jellyfin.Server.Implementations ;
2019-01-13 14:30:41 -05:00
using MediaBrowser.Common.Configuration ;
2020-10-31 14:21:46 -04:00
using MediaBrowser.Common.Net ;
2020-02-28 14:49:04 -05:00
using MediaBrowser.Controller.Extensions ;
2021-06-12 16:20:35 -04:00
using MediaBrowser.Model.IO ;
2019-11-24 09:27:58 -05:00
using Microsoft.AspNetCore.Hosting ;
2021-05-11 17:26:00 -04:00
using Microsoft.EntityFrameworkCore ;
2019-01-13 14:30:41 -05:00
using Microsoft.Extensions.Configuration ;
2019-02-03 11:09:12 -05:00
using Microsoft.Extensions.DependencyInjection ;
2019-11-24 09:27:58 -05:00
using Microsoft.Extensions.DependencyInjection.Extensions ;
2020-03-21 16:31:22 -04:00
using Microsoft.Extensions.Hosting ;
2019-01-13 14:30:41 -05:00
using Microsoft.Extensions.Logging ;
2019-10-26 17:58:23 -04:00
using Microsoft.Extensions.Logging.Abstractions ;
2019-01-13 14:30:41 -05:00
using Serilog ;
2019-09-11 13:31:35 -04:00
using Serilog.Extensions.Logging ;
2019-03-11 18:07:38 -04:00
using SQLitePCL ;
2020-09-03 05:32:22 -04:00
using ConfigurationExtensions = MediaBrowser . Controller . Extensions . ConfigurationExtensions ;
2019-01-13 14:30:41 -05:00
using ILogger = Microsoft . Extensions . Logging . ILogger ;
namespace Jellyfin.Server
{
2019-08-11 09:11:53 -04:00
/// <summary>
/// Class containing the entry point of the application.
/// </summary>
2019-01-13 14:30:41 -05:00
public static class Program
{
2020-03-05 12:09:33 -05:00
/// <summary>
2020-03-06 13:07:34 -05:00
/// The name of logging configuration file containing application defaults.
2020-03-05 12:09:33 -05:00
/// </summary>
2020-05-29 05:28:19 -04:00
public const string LoggingConfigFileDefault = "logging.default.json" ;
2020-03-06 13:07:34 -05:00
/// <summary>
2020-03-08 10:46:13 -04:00
/// The name of the logging configuration file containing the system-specific override settings.
2020-03-06 13:07:34 -05:00
/// </summary>
2020-05-29 05:28:19 -04:00
public const string LoggingConfigFileSystem = "logging.json" ;
2020-03-05 12:09:33 -05:00
2019-01-12 17:58:58 -05:00
private static readonly CancellationTokenSource _tokenSource = new CancellationTokenSource ( ) ;
private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory ( ) ;
2019-10-26 17:58:23 -04:00
private static ILogger _logger = NullLogger . Instance ;
2019-01-13 14:30:41 -05:00
private static bool _restartOnShutdown ;
2019-08-11 09:11:53 -04:00
/// <summary>
/// The entry point of the application.
/// </summary>
/// <param name="args">The command line arguments passed.</param>
/// <returns><see cref="Task" />.</returns>
2019-02-23 21:16:19 -05:00
public static Task Main ( string [ ] args )
2019-01-13 14:30:41 -05:00
{
2020-06-15 11:06:57 -04:00
static Task ErrorParsingArguments ( IEnumerable < Error > errors )
2019-01-28 09:51:31 -05:00
{
2020-06-15 11:06:57 -04:00
Environment . ExitCode = 1 ;
return Task . CompletedTask ;
2019-01-28 09:51:31 -05:00
}
2019-01-28 08:41:37 -05:00
// Parse the command line arguments and either start the app or exit indicating error
2019-02-23 21:16:19 -05:00
return Parser . Default . ParseArguments < StartupOptions > ( args )
2020-06-15 11:06:57 -04:00
. MapResult ( StartApp , ErrorParsingArguments ) ;
2019-01-28 08:41:37 -05:00
}
2019-01-13 14:30:41 -05:00
2019-08-11 09:11:53 -04:00
/// <summary>
/// Shuts down the application.
/// </summary>
internal static void Shutdown ( )
2019-02-13 11:19:55 -05:00
{
if ( ! _tokenSource . IsCancellationRequested )
{
_tokenSource . Cancel ( ) ;
}
}
2019-08-11 09:11:53 -04:00
/// <summary>
/// Restarts the application.
/// </summary>
internal static void Restart ( )
2019-02-13 11:19:55 -05:00
{
_restartOnShutdown = true ;
Shutdown ( ) ;
}
2019-01-28 08:41:37 -05:00
private static async Task StartApp ( StartupOptions options )
{
2019-09-28 18:29:28 -04:00
var stopWatch = new Stopwatch ( ) ;
stopWatch . Start ( ) ;
2019-10-26 17:58:23 -04:00
2019-10-29 12:49:41 -04:00
// Log all uncaught exceptions to std error
2019-10-26 17:58:23 -04:00
static void UnhandledExceptionToConsole ( object sender , UnhandledExceptionEventArgs e ) = >
Console . Error . WriteLine ( "Unhandled Exception\n" + e . ExceptionObject . ToString ( ) ) ;
AppDomain . CurrentDomain . UnhandledException + = UnhandledExceptionToConsole ;
2019-01-18 05:10:45 -05:00
ServerApplicationPaths appPaths = CreateApplicationPaths ( options ) ;
2019-01-13 14:30:41 -05:00
// $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager
Environment . SetEnvironmentVariable ( "JELLYFIN_LOG_DIR" , appPaths . LogDirectoryPath ) ;
2019-02-08 04:13:58 -05:00
2020-11-24 10:25:32 -05:00
// Enable cl-va P010 interop for tonemapping on Intel VAAPI
Environment . SetEnvironmentVariable ( "NEOReadDebugKeys" , "1" ) ;
Environment . SetEnvironmentVariable ( "EnableExtendedVaFormats" , "1" ) ;
2020-02-28 17:18:22 -05:00
await InitLoggingConfigFile ( appPaths ) . ConfigureAwait ( false ) ;
2020-03-15 10:34:09 -04:00
// Create an instance of the application configuration to use for application startup
IConfiguration startupConfig = CreateAppConfiguration ( options , appPaths ) ;
2019-02-08 04:13:58 -05:00
2020-02-28 17:18:22 -05:00
// Initialize logging framework
2020-02-28 17:28:15 -05:00
InitializeLoggingFramework ( startupConfig , appPaths ) ;
2019-01-13 14:30:41 -05:00
_logger = _loggerFactory . CreateLogger ( "Main" ) ;
2019-10-26 17:58:23 -04:00
// Log uncaught exceptions to the logging instead of std error
AppDomain . CurrentDomain . UnhandledException - = UnhandledExceptionToConsole ;
2021-08-04 08:40:09 -04:00
AppDomain . CurrentDomain . UnhandledException + = ( _ , e )
2019-01-13 14:30:41 -05:00
= > _logger . LogCritical ( ( Exception ) e . ExceptionObject , "Unhandled Exception" ) ;
2019-01-12 17:58:58 -05:00
// Intercept Ctrl+C and Ctrl+Break
2021-08-04 08:40:09 -04:00
Console . CancelKeyPress + = ( _ , e ) = >
2019-01-12 17:58:58 -05:00
{
2019-01-12 19:05:25 -05:00
if ( _tokenSource . IsCancellationRequested )
{
return ; // Already shutting down
}
2019-02-13 11:19:55 -05:00
2019-01-12 17:58:58 -05:00
e . Cancel = true ;
_logger . LogInformation ( "Ctrl+C, shutting down" ) ;
2019-01-12 19:05:25 -05:00
Environment . ExitCode = 128 + 2 ;
2019-01-12 17:58:58 -05:00
Shutdown ( ) ;
} ;
2019-01-12 17:31:45 -05:00
// Register a SIGTERM handler
2021-08-04 08:40:09 -04:00
AppDomain . CurrentDomain . ProcessExit + = ( _ , _ ) = >
2019-01-12 17:31:45 -05:00
{
2019-01-12 17:58:58 -05:00
if ( _tokenSource . IsCancellationRequested )
{
return ; // Already shutting down
}
2019-02-13 11:19:55 -05:00
2019-01-12 17:31:45 -05:00
_logger . LogInformation ( "Received a SIGTERM signal, shutting down" ) ;
2019-01-12 19:05:25 -05:00
Environment . ExitCode = 128 + 15 ;
2019-01-12 17:31:45 -05:00
Shutdown ( ) ;
} ;
2019-09-27 17:58:04 -04:00
_logger . LogInformation (
"Jellyfin version: {Version}" ,
2019-10-26 17:58:23 -04:00
Assembly . GetEntryAssembly ( ) ! . GetName ( ) . Version ! . ToString ( 3 ) ) ;
2019-01-13 14:30:41 -05:00
2019-03-07 11:39:40 -05:00
ApplicationHost . LogEnvironmentInfo ( _logger , appPaths ) ;
2019-01-13 14:30:41 -05:00
2020-04-20 14:58:00 -04:00
PerformStaticInitialization ( ) ;
2020-08-16 17:25:14 -04:00
var serviceCollection = new ServiceCollection ( ) ;
2019-01-13 14:30:41 -05:00
2019-08-18 14:01:08 -04:00
var appHost = new CoreAppHost (
2019-01-13 14:30:41 -05:00
appPaths ,
_loggerFactory ,
options ,
2021-02-27 15:12:55 -05:00
startupConfig ,
2019-06-09 17:51:52 -04:00
new ManagedFileSystem ( _loggerFactory . CreateLogger < ManagedFileSystem > ( ) , appPaths ) ,
2020-08-16 17:25:14 -04:00
serviceCollection ) ;
2020-03-25 13:52:14 -04:00
2019-08-18 14:01:08 -04:00
try
2019-01-13 14:30:41 -05:00
{
2020-03-25 13:52:14 -04:00
// If hosting the web client, validate the client content path
if ( startupConfig . HostWebClient ( ) )
{
Fix random failing of tests
Fully initialize the configuration manager at the init stage
```
Failed Jellyfin.Server.Integration.Tests.Controllers.ActivityLogControllerTests.ActivityLog_GetEntries_Ok [2 s]
Error Message:
MediaBrowser.Common.Extensions.ResourceNotFoundException : Configuration with key metadata not found.
Stack Trace:
at Emby.Server.Implementations.AppBase.BaseConfigurationManager.<>c__DisplayClass43_0.<GetConfiguration>b__0(String k) in D:\a\1\s\Emby.Server.Implementations\AppBase\BaseConfigurationManager.cs:line 309
at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
at Emby.Server.Implementations.AppBase.BaseConfigurationManager.GetConfiguration(String key) in D:\a\1\s\Emby.Server.Implementations\AppBase\BaseConfigurationManager.cs:line 300
at MediaBrowser.Common.Configuration.ConfigurationManagerExtensions.GetConfiguration[T](IConfigurationManager manager, String key) in D:\a\1\s\MediaBrowser.Common\Configuration\IConfigurationManager.cs:line 88
at MediaBrowser.Controller.Library.MetadataConfigurationExtensions.GetMetadataConfiguration(IConfigurationManager config) in D:\a\1\s\MediaBrowser.Controller\Library\MetadataConfigurationStore.cs:line 28
at Emby.Server.Implementations.Library.ResolverHelper.SetDateCreated(BaseItem item, IFileSystem fileSystem, FileSystemMetadata info) in D:\a\1\s\Emby.Server.Implementations\Library\ResolverHelper.cs:line 159
at Emby.Server.Implementations.Library.ResolverHelper.EnsureDates(IFileSystem fileSystem, BaseItem item, ItemResolveArgs args) in D:\a\1\s\Emby.Server.Implementations\Library\ResolverHelper.cs:line 153
at Emby.Server.Implementations.Library.ResolverHelper.SetInitialItemValues(BaseItem item, ItemResolveArgs args, IFileSystem fileSystem, ILibraryManager libraryManager) in D:\a\1\s\Emby.Server.Implementations\Library\ResolverHelper.cs:line 81
at Emby.Server.Implementations.Library.LibraryManager.ResolveItem(ItemResolveArgs args, IItemResolver[] resolvers) in D:\a\1\s\Emby.Server.Implementations\Library\LibraryManager.cs:line 480
at Emby.Server.Implementations.Library.LibraryManager.ResolvePath(FileSystemMetadata fileInfo, IDirectoryService directoryService, IItemResolver[] resolvers, Folder parent, String collectionType, LibraryOptions libraryOptions) in D:\a\1\s\Emby.Server.Implementations\Library\LibraryManager.cs:line 618
at Emby.Server.Implementations.Library.LibraryManager.ResolvePath(FileSystemMetadata fileInfo, Folder parent) in D:\a\1\s\Emby.Server.Implementations\Library\LibraryManager.cs:line 536
at Emby.Server.Implementations.Library.LibraryManager.CreateRootFolder() in D:\a\1\s\Emby.Server.Implementations\Library\LibraryManager.cs:line 732
at Emby.Server.Implementations.Library.LibraryManager.get_RootFolder() in D:\a\1\s\Emby.Server.Implementations\Library\LibraryManager.cs:line 180
at Emby.Server.Implementations.IO.LibraryMonitor.Start() in D:\a\1\s\Emby.Server.Implementations\IO\LibraryMonitor.cs:line 135
at Emby.Server.Implementations.IO.LibraryMonitorStartup.RunAsync() in D:\a\1\s\Emby.Server.Implementations\IO\LibraryMonitorStartup.cs:line 26
at Emby.Server.Implementations.ApplicationHost.StartEntryPoints(IEnumerable`1 entryPoints, Boolean isBeforeStartup)+MoveNext() in D:\a\1\s\Emby.Server.Implementations\ApplicationHost.cs:line 541
at System.Threading.Tasks.Task.WhenAll(IEnumerable`1 tasks)
at Emby.Server.Implementations.ApplicationHost.RunStartupTasksAsync(CancellationToken cancellationToken) in D:\a\1\s\Emby.Server.Implementations\ApplicationHost.cs:line 525
at Jellyfin.Server.Integration.Tests.JellyfinApplicationFactory.CreateServer(IWebHostBuilder builder) in D:\a\1\s\tests\Jellyfin.Server.Integration.Tests\JellyfinApplicationFactory.cs:line 101
at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.EnsureServer()
at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.CreateDefaultClient(DelegatingHandler[] handlers)
at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.CreateDefaultClient(Uri baseAddress, DelegatingHandler[] handlers)
at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.CreateClient(WebApplicationFactoryClientOptions options)
at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.CreateClient()
at Jellyfin.Server.Integration.Tests.Controllers.ActivityLogControllerTests.ActivityLog_GetEntries_Ok() in D:\a\1\s\tests\Jellyfin.Server.Integration.Tests\Controllers\ActivityLogControllerTests.cs:line 21
--- End of stack trace from previous location ---
```
2021-04-11 18:28:17 -04:00
string? webContentPath = appHost . ConfigurationManager . ApplicationPaths . WebPath ;
2020-03-25 13:52:14 -04:00
if ( ! Directory . Exists ( webContentPath ) | | Directory . GetFiles ( webContentPath ) . Length = = 0 )
{
throw new InvalidOperationException (
"The server is expected to host the web client, but the provided content directory is either " +
$"invalid or empty: {webContentPath}. If you do not want to host the web client with the " +
"server, you may set the '--nowebclient' command line flag, or set" +
2021-08-04 08:40:09 -04:00
$"'{ConfigurationExtensions.HostWebClientKey}=false' in your config settings." ) ;
2020-03-25 13:52:14 -04:00
}
}
2020-08-16 17:25:14 -04:00
appHost . Init ( ) ;
2019-11-24 09:27:58 -05:00
2020-04-20 14:58:00 -04:00
var webHost = new WebHostBuilder ( ) . ConfigureWebHostBuilder ( appHost , serviceCollection , options , startupConfig , appPaths ) . Build ( ) ;
2019-11-24 09:27:58 -05:00
2020-03-15 10:23:50 -04:00
// Re-use the web host service provider in the app host since ASP.NET doesn't allow a custom service collection.
2020-02-28 17:18:22 -05:00
appHost . ServiceProvider = webHost . Services ;
2020-04-04 20:21:48 -04:00
await appHost . InitializeServices ( ) . ConfigureAwait ( false ) ;
2020-03-05 12:09:33 -05:00
Migrations . MigrationRunner . Run ( appHost , _loggerFactory ) ;
2019-11-24 09:27:58 -05:00
try
{
2020-02-28 17:18:22 -05:00
await webHost . StartAsync ( ) . ConfigureAwait ( false ) ;
2019-11-24 09:27:58 -05:00
}
catch
{
2021-03-01 16:02:20 -05:00
_logger . LogError ( "Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in network.xml and try again." ) ;
2019-11-24 09:27:58 -05:00
throw ;
}
2019-01-13 14:30:41 -05:00
2021-02-23 11:30:24 -05:00
await appHost . RunStartupTasksAsync ( _tokenSource . Token ) . ConfigureAwait ( false ) ;
2019-01-25 15:33:58 -05:00
2019-09-28 18:29:28 -04:00
stopWatch . Stop ( ) ;
_logger . LogInformation ( "Startup complete {Time:g}" , stopWatch . Elapsed ) ;
2019-08-18 14:01:08 -04:00
// Block main thread until shutdown
await Task . Delay ( - 1 , _tokenSource . Token ) . ConfigureAwait ( false ) ;
}
catch ( TaskCanceledException )
{
// Don't throw on cancellation
}
catch ( Exception ex )
{
_logger . LogCritical ( ex , "Error while starting server." ) ;
}
finally
{
2021-05-11 17:26:00 -04:00
_logger . LogInformation ( "Running query planner optimizations in the database... This might take a while" ) ;
2021-05-24 04:48:01 -04:00
// Run before disposing the application
2021-05-25 10:47:29 -04:00
using var context = appHost . Resolve < JellyfinDbProvider > ( ) . CreateContext ( ) ;
2021-05-11 17:26:00 -04:00
if ( context . Database . IsSqlite ( ) )
{
context . Database . ExecuteSqlRaw ( "PRAGMA optimize" ) ;
}
2021-05-24 04:48:01 -04:00
appHost . Dispose ( ) ;
2019-01-13 14:30:41 -05:00
}
if ( _restartOnShutdown )
{
StartNewInstance ( options ) ;
}
}
2020-04-20 14:58:00 -04:00
/// <summary>
/// Call static initialization methods for the application.
/// </summary>
public static void PerformStaticInitialization ( )
{
// Make sure we have all the code pages we can get
// Ref: https://docs.microsoft.com/en-us/dotnet/api/system.text.codepagesencodingprovider.instance?view=netcore-3.0#remarks
Encoding . RegisterProvider ( CodePagesEncodingProvider . Instance ) ;
// Increase the max http request limit
// The default connection limit is 10 for ASP.NET hosted applications and 2 for all others.
ServicePointManager . DefaultConnectionLimit = Math . Max ( 96 , ServicePointManager . DefaultConnectionLimit ) ;
// Disable the "Expect: 100-Continue" header by default
// http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c
ServicePointManager . Expect100Continue = false ;
Batteries_V2 . Init ( ) ;
if ( raw . sqlite3_enable_shared_cache ( 1 ) ! = raw . SQLITE_OK )
{
_logger . LogWarning ( "Failed to enable shared cache for SQLite" ) ;
}
}
/// <summary>
/// Configure the web host builder.
/// </summary>
/// <param name="builder">The builder to configure.</param>
/// <param name="appHost">The application host.</param>
/// <param name="serviceCollection">The application service collection.</param>
/// <param name="commandLineOpts">The command line options passed to the application.</param>
/// <param name="startupConfig">The application configuration.</param>
/// <param name="appPaths">The application paths.</param>
/// <returns>The configured web host builder.</returns>
public static IWebHostBuilder ConfigureWebHostBuilder (
this IWebHostBuilder builder ,
2020-03-11 18:04:47 -04:00
ApplicationHost appHost ,
IServiceCollection serviceCollection ,
2020-03-15 10:34:09 -04:00
StartupOptions commandLineOpts ,
2020-03-11 18:04:47 -04:00
IConfiguration startupConfig ,
IApplicationPaths appPaths )
2019-11-24 09:27:58 -05:00
{
2020-04-20 14:58:00 -04:00
return builder
2020-03-21 16:31:22 -04:00
. UseKestrel ( ( builderContext , options ) = >
2019-11-24 09:27:58 -05:00
{
2020-10-31 14:21:46 -04:00
var addresses = appHost . NetManager . GetAllBindInterfaces ( ) ;
2020-04-08 06:41:11 -04:00
2020-09-12 11:41:37 -04:00
bool flagged = false ;
foreach ( IPObject netAdd in addresses )
2019-11-24 09:27:58 -05:00
{
2021-03-02 03:57:27 -05:00
_logger . LogInformation ( "Kestrel listening on {Address}" , netAdd . Address = = IPAddress . IPv6Any ? "All Addresses" : netAdd ) ;
2020-09-12 11:41:37 -04:00
options . Listen ( netAdd . Address , appHost . HttpPort ) ;
2020-04-02 17:45:04 -04:00
if ( appHost . ListenWithHttps )
2019-11-24 09:27:58 -05:00
{
2020-11-04 15:17:41 -05:00
options . Listen (
2020-11-04 15:38:47 -05:00
netAdd . Address ,
appHost . HttpsPort ,
2020-10-31 07:49:41 -04:00
listenOptions = > listenOptions . UseHttps ( appHost . Certificate ) ) ;
2019-11-24 09:27:58 -05:00
}
2020-03-21 16:31:22 -04:00
else if ( builderContext . HostingEnvironment . IsDevelopment ( ) )
{
2020-04-26 14:35:36 -04:00
try
2020-03-21 16:31:22 -04:00
{
2020-11-04 15:17:41 -05:00
options . Listen (
netAdd . Address ,
appHost . HttpsPort ,
2020-11-04 15:38:47 -05:00
listenOptions = > listenOptions . UseHttps ( ) ) ;
2020-04-23 13:30:48 -04:00
}
2020-09-12 11:41:37 -04:00
catch ( InvalidOperationException )
2020-04-26 14:35:36 -04:00
{
2020-09-12 11:41:37 -04:00
if ( ! flagged )
{
_logger . LogWarning ( "Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted." ) ;
flagged = true ;
}
2020-04-26 14:35:36 -04:00
}
2020-03-21 16:31:22 -04:00
}
2019-11-24 09:27:58 -05:00
}
2020-07-11 06:35:18 -04:00
2021-07-12 14:20:50 -04:00
// Bind to unix socket (only on unix systems)
if ( startupConfig . UseUnixSocket ( ) & & Environment . OSVersion . Platform = = PlatformID . Unix )
2020-07-11 06:35:18 -04:00
{
2020-07-23 07:18:47 -04:00
var socketPath = startupConfig . GetUnixSocketPath ( ) ;
if ( string . IsNullOrEmpty ( socketPath ) )
{
var xdgRuntimeDir = Environment . GetEnvironmentVariable ( "XDG_RUNTIME_DIR" ) ;
if ( xdgRuntimeDir = = null )
{
// Fall back to config dir
socketPath = Path . Join ( appPaths . ConfigurationDirectoryPath , "socket.sock" ) ;
}
else
{
socketPath = Path . Join ( xdgRuntimeDir , "jellyfin-socket" ) ;
}
}
2020-07-11 06:35:18 -04:00
// Workaround for https://github.com/aspnet/AspNetCore/issues/14134
if ( File . Exists ( socketPath ) )
{
File . Delete ( socketPath ) ;
}
options . ListenUnixSocket ( socketPath ) ;
2020-07-23 06:03:46 -04:00
_logger . LogInformation ( "Kestrel listening to unix socket {SocketPath}" , socketPath ) ;
2020-07-11 06:35:18 -04:00
}
2019-11-24 09:27:58 -05:00
} )
2020-03-15 10:34:09 -04:00
. ConfigureAppConfiguration ( config = > config . ConfigureAppConfiguration ( commandLineOpts , appPaths , startupConfig ) )
2020-03-02 18:35:41 -05:00
. UseSerilog ( )
2019-11-24 09:27:58 -05:00
. ConfigureServices ( services = >
{
// Merge the external ServiceCollection into ASP.NET DI
2020-10-06 08:44:07 -04:00
services . Add ( serviceCollection ) ;
2019-11-24 09:27:58 -05:00
} )
. UseStartup < Startup > ( ) ;
}
2019-02-13 10:35:14 -05:00
/// <summary>
/// Create the data, config and log paths from the variety of inputs(command line args,
2019-06-09 17:51:52 -04:00
/// environment variables) or decide on what default to use. For Windows it's %AppPath%
2019-08-11 09:11:53 -04:00
/// for everything else the
2019-08-11 09:17:39 -04:00
/// <a href="https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html">XDG approach</a>
2019-08-11 09:11:53 -04:00
/// is followed.
2019-02-13 10:35:14 -05:00
/// </summary>
2019-08-11 09:11:53 -04:00
/// <param name="options">The <see cref="StartupOptions" /> for this instance.</param>
/// <returns><see cref="ServerApplicationPaths" />.</returns>
2019-01-18 05:10:45 -05:00
private static ServerApplicationPaths CreateApplicationPaths ( StartupOptions options )
2019-01-13 14:30:41 -05:00
{
2019-02-13 10:35:14 -05:00
// dataDir
// IF --datadir
2019-03-10 18:24:11 -04:00
// ELSE IF $JELLYFIN_DATA_DIR
2019-02-13 10:35:14 -05:00
// ELSE IF windows, use <%APPDATA%>/jellyfin
// ELSE IF $XDG_DATA_HOME then use $XDG_DATA_HOME/jellyfin
// ELSE use $HOME/.local/share/jellyfin
var dataDir = options . DataDir ;
if ( string . IsNullOrEmpty ( dataDir ) )
2019-01-13 14:30:41 -05:00
{
2019-03-10 18:24:11 -04:00
dataDir = Environment . GetEnvironmentVariable ( "JELLYFIN_DATA_DIR" ) ;
2019-02-13 10:35:14 -05:00
if ( string . IsNullOrEmpty ( dataDir ) )
2019-01-13 14:30:41 -05:00
{
2019-02-09 14:20:39 -05:00
// LocalApplicationData follows the XDG spec on unix machines
2019-06-09 17:51:52 -04:00
dataDir = Path . Combine (
Environment . GetFolderPath ( Environment . SpecialFolder . LocalApplicationData ) ,
"jellyfin" ) ;
2019-01-13 14:30:41 -05:00
}
}
2019-02-13 10:35:14 -05:00
// configDir
// IF --configdir
// ELSE IF $JELLYFIN_CONFIG_DIR
// ELSE IF --datadir, use <datadir>/config (assume portable run)
// ELSE IF <datadir>/config exists, use that
// ELSE IF windows, use <datadir>/config
// ELSE IF $XDG_CONFIG_HOME use $XDG_CONFIG_HOME/jellyfin
// ELSE $HOME/.config/jellyfin
var configDir = options . ConfigDir ;
2019-01-13 14:30:41 -05:00
if ( string . IsNullOrEmpty ( configDir ) )
{
2019-02-13 10:35:14 -05:00
configDir = Environment . GetEnvironmentVariable ( "JELLYFIN_CONFIG_DIR" ) ;
if ( string . IsNullOrEmpty ( configDir ) )
2019-01-13 14:30:41 -05:00
{
2019-06-09 17:51:52 -04:00
if ( options . DataDir ! = null
| | Directory . Exists ( Path . Combine ( dataDir , "config" ) )
2021-07-12 14:20:50 -04:00
| | OperatingSystem . IsWindows ( ) )
2019-02-13 10:35:14 -05:00
{
// Hang config folder off already set dataDir
configDir = Path . Combine ( dataDir , "config" ) ;
}
else
{
2019-06-09 17:51:52 -04:00
// $XDG_CONFIG_HOME defines the base directory relative to which
// user specific configuration files should be stored.
2019-02-13 10:35:14 -05:00
configDir = Environment . GetEnvironmentVariable ( "XDG_CONFIG_HOME" ) ;
2019-06-09 17:51:52 -04:00
// If $XDG_CONFIG_HOME is either not set or empty,
// a default equal to $HOME /.config should be used.
2019-02-13 10:35:14 -05:00
if ( string . IsNullOrEmpty ( configDir ) )
{
2019-06-09 17:51:52 -04:00
configDir = Path . Combine (
Environment . GetFolderPath ( Environment . SpecialFolder . UserProfile ) ,
".config" ) ;
2019-02-13 10:35:14 -05:00
}
configDir = Path . Combine ( configDir , "jellyfin" ) ;
}
2019-01-13 14:30:41 -05:00
}
}
2019-02-13 10:35:14 -05:00
// cacheDir
// IF --cachedir
// ELSE IF $JELLYFIN_CACHE_DIR
// ELSE IF windows, use <datadir>/cache
// ELSE IF XDG_CACHE_HOME, use $XDG_CACHE_HOME/jellyfin
// ELSE HOME/.cache/jellyfin
var cacheDir = options . CacheDir ;
2019-01-28 11:52:56 -05:00
if ( string . IsNullOrEmpty ( cacheDir ) )
{
2019-02-13 10:35:14 -05:00
cacheDir = Environment . GetEnvironmentVariable ( "JELLYFIN_CACHE_DIR" ) ;
if ( string . IsNullOrEmpty ( cacheDir ) )
2019-01-28 11:52:56 -05:00
{
2021-07-12 14:20:50 -04:00
if ( OperatingSystem . IsWindows ( ) )
2019-02-13 10:35:14 -05:00
{
// Hang cache folder off already set dataDir
cacheDir = Path . Combine ( dataDir , "cache" ) ;
}
else
2019-01-28 11:52:56 -05:00
{
2019-06-09 17:51:52 -04:00
// $XDG_CACHE_HOME defines the base directory relative to which
// user specific non-essential data files should be stored.
2019-02-13 10:35:14 -05:00
cacheDir = Environment . GetEnvironmentVariable ( "XDG_CACHE_HOME" ) ;
2019-06-09 17:51:52 -04:00
// If $XDG_CACHE_HOME is either not set or empty,
// a default equal to $HOME/.cache should be used.
2019-02-13 10:35:14 -05:00
if ( string . IsNullOrEmpty ( cacheDir ) )
{
2019-06-09 17:51:52 -04:00
cacheDir = Path . Combine (
Environment . GetFolderPath ( Environment . SpecialFolder . UserProfile ) ,
".cache" ) ;
2019-02-13 10:35:14 -05:00
}
cacheDir = Path . Combine ( cacheDir , "jellyfin" ) ;
2019-01-28 11:52:56 -05:00
}
}
}
2019-03-10 16:17:48 -04:00
// webDir
// IF --webdir
// ELSE IF $JELLYFIN_WEB_DIR
2020-02-25 10:51:36 -05:00
// ELSE <bindir>/jellyfin-web
2019-03-10 16:17:48 -04:00
var webDir = options . WebDir ;
if ( string . IsNullOrEmpty ( webDir ) )
{
webDir = Environment . GetEnvironmentVariable ( "JELLYFIN_WEB_DIR" ) ;
if ( string . IsNullOrEmpty ( webDir ) )
{
// Use default location under ResourcesPath
2019-09-24 10:22:26 -04:00
webDir = Path . Combine ( AppContext . BaseDirectory , "jellyfin-web" ) ;
2019-03-10 16:17:48 -04:00
}
}
2019-02-13 10:35:14 -05:00
// logDir
// IF --logdir
// ELSE IF $JELLYFIN_LOG_DIR
// ELSE IF --datadir, use <datadir>/log (assume portable run)
// ELSE <datadir>/log
var logDir = options . LogDir ;
2019-01-13 14:30:41 -05:00
if ( string . IsNullOrEmpty ( logDir ) )
{
2019-02-13 10:35:14 -05:00
logDir = Environment . GetEnvironmentVariable ( "JELLYFIN_LOG_DIR" ) ;
if ( string . IsNullOrEmpty ( logDir ) )
2019-01-13 14:30:41 -05:00
{
2019-02-13 10:35:14 -05:00
// Hang log folder off already set dataDir
logDir = Path . Combine ( dataDir , "log" ) ;
2019-01-13 14:30:41 -05:00
}
}
2020-09-11 04:34:47 -04:00
// Normalize paths. Only possible with GetFullPath for now - https://github.com/dotnet/runtime/issues/2162
dataDir = Path . GetFullPath ( dataDir ) ;
logDir = Path . GetFullPath ( logDir ) ;
configDir = Path . GetFullPath ( configDir ) ;
cacheDir = Path . GetFullPath ( cacheDir ) ;
webDir = Path . GetFullPath ( webDir ) ;
2019-02-13 10:35:14 -05:00
// Ensure the main folders exist before we continue
try
2019-01-18 05:10:45 -05:00
{
2019-03-10 18:30:10 -04:00
Directory . CreateDirectory ( dataDir ) ;
2019-01-18 05:10:45 -05:00
Directory . CreateDirectory ( logDir ) ;
2019-02-13 10:35:14 -05:00
Directory . CreateDirectory ( configDir ) ;
Directory . CreateDirectory ( cacheDir ) ;
}
catch ( IOException ex )
{
Console . Error . WriteLine ( "Error whilst attempting to create folder" ) ;
Console . Error . WriteLine ( ex . ToString ( ) ) ;
Environment . Exit ( 1 ) ;
2019-01-18 05:10:45 -05:00
}
2019-03-10 16:17:48 -04:00
return new ServerApplicationPaths ( dataDir , logDir , configDir , cacheDir , webDir ) ;
2019-01-13 14:30:41 -05:00
}
2020-02-28 17:18:22 -05:00
/// <summary>
/// Initialize the logging configuration file using the bundled resource file as a default if it doesn't exist
/// already.
/// </summary>
2020-04-20 14:58:00 -04:00
/// <param name="appPaths">The application paths.</param>
/// <returns>A task representing the creation of the configuration file, or a completed task if the file already exists.</returns>
public static async Task InitLoggingConfigFile ( IApplicationPaths appPaths )
2019-01-13 14:30:41 -05:00
{
2020-02-28 17:18:22 -05:00
// Do nothing if the config file already exists
2020-03-06 13:07:34 -05:00
string configPath = Path . Combine ( appPaths . ConfigurationDirectoryPath , LoggingConfigFileDefault ) ;
2020-02-28 17:18:22 -05:00
if ( File . Exists ( configPath ) )
2019-02-08 04:13:58 -05:00
{
2020-02-28 17:18:22 -05:00
return ;
}
2020-01-31 16:23:46 -05:00
2020-02-28 17:18:22 -05:00
// Get a stream of the resource contents
// NOTE: The .csproj name is used instead of the assembly name in the resource path
const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json" ;
2021-08-04 08:40:09 -04:00
await using Stream resource = typeof ( Program ) . Assembly . GetManifestResourceStream ( ResourcePath )
2020-03-10 17:45:17 -04:00
? ? throw new InvalidOperationException ( $"Invalid resource path: '{ResourcePath}'" ) ;
2019-02-13 11:19:55 -05:00
2020-02-28 17:18:22 -05:00
// Copy the resource contents to the expected file path for the config file
2021-06-12 16:20:35 -04:00
await using Stream dst = new FileStream ( configPath , FileMode . CreateNew , FileAccess . Write , FileShare . None , IODefaults . FileStreamBufferSize , AsyncFile . UseAsyncIO ) ;
2020-02-28 17:18:22 -05:00
await resource . CopyToAsync ( dst ) . ConfigureAwait ( false ) ;
}
2020-04-20 14:58:00 -04:00
/// <summary>
/// Create the application configuration.
/// </summary>
/// <param name="commandLineOpts">The command line options passed to the program.</param>
/// <param name="appPaths">The application paths.</param>
/// <returns>The application configuration.</returns>
public static IConfiguration CreateAppConfiguration ( StartupOptions commandLineOpts , IApplicationPaths appPaths )
2020-02-28 17:18:22 -05:00
{
2019-02-08 04:13:58 -05:00
return new ConfigurationBuilder ( )
2020-03-15 10:34:09 -04:00
. ConfigureAppConfiguration ( commandLineOpts , appPaths )
2020-02-28 17:18:22 -05:00
. Build ( ) ;
}
2020-03-15 10:34:09 -04:00
private static IConfigurationBuilder ConfigureAppConfiguration (
this IConfigurationBuilder config ,
StartupOptions commandLineOpts ,
IApplicationPaths appPaths ,
IConfiguration ? startupConfig = null )
2020-02-28 17:18:22 -05:00
{
2020-03-21 13:25:09 -04:00
// Use the swagger API page as the default redirect path if not hosting the web client
2020-02-25 11:02:51 -05:00
var inMemoryDefaultConfig = ConfigurationOptions . DefaultConfiguration ;
2020-03-21 13:25:09 -04:00
if ( startupConfig ! = null & & ! startupConfig . HostWebClient ( ) )
2020-02-25 11:02:51 -05:00
{
2020-09-03 05:32:22 -04:00
inMemoryDefaultConfig [ ConfigurationExtensions . DefaultRedirectKey ] = "api-docs/swagger" ;
2020-02-25 11:02:51 -05:00
}
2020-02-28 17:18:22 -05:00
return config
2019-02-08 04:13:58 -05:00
. SetBasePath ( appPaths . ConfigurationDirectoryPath )
2020-02-25 11:02:51 -05:00
. AddInMemoryCollection ( inMemoryDefaultConfig )
2020-03-06 13:28:36 -05:00
. AddJsonFile ( LoggingConfigFileDefault , optional : false , reloadOnChange : true )
2020-03-08 10:46:13 -04:00
. AddJsonFile ( LoggingConfigFileSystem , optional : true , reloadOnChange : true )
2020-03-15 10:34:09 -04:00
. AddEnvironmentVariables ( "JELLYFIN_" )
. AddInMemoryCollection ( commandLineOpts . ConvertToConfig ( ) ) ;
2019-02-08 04:13:58 -05:00
}
2019-01-13 14:30:41 -05:00
2020-02-28 17:18:22 -05:00
/// <summary>
/// Initialize Serilog using configuration and fall back to defaults on failure.
/// </summary>
private static void InitializeLoggingFramework ( IConfiguration configuration , IApplicationPaths appPaths )
2019-02-08 04:13:58 -05:00
{
try
{
2019-01-13 14:30:41 -05:00
// Serilog.Log is used by SerilogLoggerFactory when no logger is specified
Serilog . Log . Logger = new LoggerConfiguration ( )
. ReadFrom . Configuration ( configuration )
. Enrich . FromLogContext ( )
2020-01-13 13:44:17 -05:00
. Enrich . WithThreadId ( )
2019-01-13 14:30:41 -05:00
. CreateLogger ( ) ;
}
catch ( Exception ex )
{
Serilog . Log . Logger = new LoggerConfiguration ( )
2020-01-16 07:20:01 -05:00
. WriteTo . Console ( outputTemplate : "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}" )
2019-01-13 14:30:41 -05:00
. WriteTo . Async ( x = > x . File (
Path . Combine ( appPaths . LogDirectoryPath , "log_.log" ) ,
rollingInterval : RollingInterval . Day ,
2020-12-30 12:31:26 -05:00
outputTemplate : "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}" ,
encoding : Encoding . UTF8 ) )
2019-01-13 14:30:41 -05:00
. Enrich . FromLogContext ( )
2020-01-13 13:44:17 -05:00
. Enrich . WithThreadId ( )
2019-01-13 14:30:41 -05:00
. CreateLogger ( ) ;
Serilog . Log . Logger . Fatal ( ex , "Failed to create/read logger configuration" ) ;
}
}
2019-01-28 08:41:37 -05:00
private static void StartNewInstance ( StartupOptions options )
2019-01-13 14:30:41 -05:00
{
_logger . LogInformation ( "Starting new instance" ) ;
2019-10-26 17:58:23 -04:00
var module = options . RestartPath ;
2019-01-13 14:30:41 -05:00
if ( string . IsNullOrWhiteSpace ( module ) )
{
2019-06-09 17:51:52 -04:00
module = Environment . GetCommandLineArgs ( ) [ 0 ] ;
2019-01-13 14:30:41 -05:00
}
string commandLineArgsString ;
2019-01-28 08:41:37 -05:00
if ( options . RestartArgs ! = null )
2019-01-13 14:30:41 -05:00
{
2021-03-08 23:57:38 -05:00
commandLineArgsString = options . RestartArgs ;
2019-01-13 14:30:41 -05:00
}
else
{
2019-01-23 14:08:50 -05:00
commandLineArgsString = string . Join (
2019-06-09 17:51:52 -04:00
' ' ,
2019-01-23 14:08:50 -05:00
Environment . GetCommandLineArgs ( ) . Skip ( 1 ) . Select ( NormalizeCommandLineArgument ) ) ;
2019-01-13 14:30:41 -05:00
}
_logger . LogInformation ( "Executable: {0}" , module ) ;
_logger . LogInformation ( "Arguments: {0}" , commandLineArgsString ) ;
Process . Start ( module , commandLineArgsString ) ;
}
private static string NormalizeCommandLineArgument ( string arg )
{
if ( ! arg . Contains ( " " , StringComparison . OrdinalIgnoreCase ) )
{
return arg ;
}
return "\"" + arg + "\"" ;
}
}
}