From 99ea6059c7493ac4ee65980abe631df4969112e9 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Tue, 6 Feb 2024 14:45:44 -0500 Subject: [PATCH 1/9] Use IHostedService for UPnP port forwarding --- Jellyfin.Server/Startup.cs | 1 + ...ortForwarding.cs => PortForwardingHost.cs} | 71 +++++++++---------- 2 files changed, 35 insertions(+), 37 deletions(-) rename src/Jellyfin.Networking/{ExternalPortForwarding.cs => PortForwardingHost.cs} (76%) diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 7d5f22545..7cf7d75da 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -125,6 +125,7 @@ namespace Jellyfin.Server services.AddLiveTvServices(); services.AddHostedService(); + services.AddHostedService(); } /// diff --git a/src/Jellyfin.Networking/ExternalPortForwarding.cs b/src/Jellyfin.Networking/PortForwardingHost.cs similarity index 76% rename from src/Jellyfin.Networking/ExternalPortForwarding.cs rename to src/Jellyfin.Networking/PortForwardingHost.cs index df9e43ca9..d01343624 100644 --- a/src/Jellyfin.Networking/ExternalPortForwarding.cs +++ b/src/Jellyfin.Networking/PortForwardingHost.cs @@ -1,7 +1,3 @@ -#nullable disable - -#pragma warning disable CS1591 - using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -12,36 +8,34 @@ using System.Threading.Tasks; using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Plugins; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Mono.Nat; namespace Jellyfin.Networking; /// -/// Server entrypoint handling external port forwarding. +/// responsible for UPnP port forwarding. /// -public sealed class ExternalPortForwarding : IServerEntryPoint +public sealed class PortForwardingHost : IHostedService, IDisposable { private readonly IServerApplicationHost _appHost; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IServerConfigurationManager _config; + private readonly ConcurrentDictionary _createdRules = new(); - private readonly ConcurrentDictionary _createdRules = new ConcurrentDictionary(); - - private Timer _timer; - private string _configIdentifier; - + private Timer? _timer; + private string? _configIdentifier; private bool _disposed; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The logger. /// The application host. /// The configuration manager. - public ExternalPortForwarding( - ILogger logger, + public PortForwardingHost( + ILogger logger, IServerApplicationHost appHost, IServerConfigurationManager config) { @@ -66,7 +60,7 @@ public sealed class ExternalPortForwarding : IServerEntryPoint .ToString(); } - private void OnConfigurationUpdated(object sender, EventArgs e) + private void OnConfigurationUpdated(object? sender, EventArgs e) { var oldConfigIdentifier = _configIdentifier; _configIdentifier = GetConfigIdentifier(); @@ -79,7 +73,7 @@ public sealed class ExternalPortForwarding : IServerEntryPoint } /// - public Task RunAsync() + public Task StartAsync(CancellationToken cancellationToken) { Start(); @@ -88,6 +82,14 @@ public sealed class ExternalPortForwarding : IServerEntryPoint return Task.CompletedTask; } + /// + public Task StopAsync(CancellationToken cancellationToken) + { + Stop(); + + return Task.CompletedTask; + } + private void Start() { var config = _config.GetNetworkConfiguration(); @@ -101,7 +103,8 @@ public sealed class ExternalPortForwarding : IServerEntryPoint NatUtility.DeviceFound += OnNatUtilityDeviceFound; NatUtility.StartDiscovery(); - _timer = new Timer((_) => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10)); + _timer?.Dispose(); + _timer = new Timer(_ => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10)); } private void Stop() @@ -112,13 +115,23 @@ public sealed class ExternalPortForwarding : IServerEntryPoint NatUtility.DeviceFound -= OnNatUtilityDeviceFound; _timer?.Dispose(); + _timer = null; } - private async void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e) + private async void OnNatUtilityDeviceFound(object? sender, DeviceEventArgs e) { + ObjectDisposedException.ThrowIf(_disposed, this); + try { - await CreateRules(e.Device).ConfigureAwait(false); + // On some systems the device discovered event seems to fire repeatedly + // This check will help ensure we're not trying to port map the same device over and over + if (!_createdRules.TryAdd(e.Device.DeviceEndpoint, 0)) + { + return; + } + + await Task.WhenAll(CreatePortMaps(e.Device)).ConfigureAwait(false); } catch (Exception ex) { @@ -126,20 +139,6 @@ public sealed class ExternalPortForwarding : IServerEntryPoint } } - private Task CreateRules(INatDevice device) - { - ObjectDisposedException.ThrowIf(_disposed, this); - - // On some systems the device discovered event seems to fire repeatedly - // This check will help ensure we're not trying to port map the same device over and over - if (!_createdRules.TryAdd(device.DeviceEndpoint, 0)) - { - return Task.CompletedTask; - } - - return Task.WhenAll(CreatePortMaps(device)); - } - private IEnumerable CreatePortMaps(INatDevice device) { var config = _config.GetNetworkConfiguration(); @@ -185,8 +184,6 @@ public sealed class ExternalPortForwarding : IServerEntryPoint _config.ConfigurationUpdated -= OnConfigurationUpdated; - Stop(); - _timer?.Dispose(); _timer = null; From d986a824cde349e2e4d7e0bff34356ff364d5e74 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Tue, 6 Feb 2024 15:35:13 -0500 Subject: [PATCH 2/9] Use IHostedService for device access management --- .../Users/DeviceAccessEntryPoint.cs | 64 ---------------- .../Users/DeviceAccessHost.cs | 76 +++++++++++++++++++ 2 files changed, 76 insertions(+), 64 deletions(-) delete mode 100644 Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs create mode 100644 Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs diff --git a/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs b/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs deleted file mode 100644 index a471ea1d5..000000000 --- a/Jellyfin.Server.Implementations/Users/DeviceAccessEntryPoint.cs +++ /dev/null @@ -1,64 +0,0 @@ -#pragma warning disable CS1591 - -using System.Threading.Tasks; -using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; -using Jellyfin.Data.Events; -using Jellyfin.Data.Queries; -using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Plugins; -using MediaBrowser.Controller.Session; - -namespace Jellyfin.Server.Implementations.Users -{ - public sealed class DeviceAccessEntryPoint : IServerEntryPoint - { - private readonly IUserManager _userManager; - private readonly IDeviceManager _deviceManager; - private readonly ISessionManager _sessionManager; - - public DeviceAccessEntryPoint(IUserManager userManager, IDeviceManager deviceManager, ISessionManager sessionManager) - { - _userManager = userManager; - _deviceManager = deviceManager; - _sessionManager = sessionManager; - } - - public Task RunAsync() - { - _userManager.OnUserUpdated += OnUserUpdated; - - return Task.CompletedTask; - } - - public void Dispose() - { - } - - private async void OnUserUpdated(object? sender, GenericEventArgs e) - { - var user = e.Argument; - if (!user.HasPermission(PermissionKind.EnableAllDevices)) - { - await UpdateDeviceAccess(user).ConfigureAwait(false); - } - } - - private async Task UpdateDeviceAccess(User user) - { - var existing = (await _deviceManager.GetDevices(new DeviceQuery - { - UserId = user.Id - }).ConfigureAwait(false)).Items; - - foreach (var device in existing) - { - if (!string.IsNullOrEmpty(device.DeviceId) && !_deviceManager.CanAccessDevice(user, device.DeviceId)) - { - await _sessionManager.Logout(device).ConfigureAwait(false); - } - } - } - } -} diff --git a/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs b/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs new file mode 100644 index 000000000..e40b541a3 --- /dev/null +++ b/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs @@ -0,0 +1,76 @@ +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; +using Jellyfin.Data.Events; +using Jellyfin.Data.Queries; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Session; +using Microsoft.Extensions.Hosting; + +namespace Jellyfin.Server.Implementations.Users; + +/// +/// responsible for managing user device permissions. +/// +public sealed class DeviceAccessHost : IHostedService +{ + private readonly IUserManager _userManager; + private readonly IDeviceManager _deviceManager; + private readonly ISessionManager _sessionManager; + + /// + /// Initializes a new instance of the class. + /// + /// The . + /// The . + /// The . + public DeviceAccessHost(IUserManager userManager, IDeviceManager deviceManager, ISessionManager sessionManager) + { + _userManager = userManager; + _deviceManager = deviceManager; + _sessionManager = sessionManager; + } + + /// + public Task StartAsync(CancellationToken cancellationToken) + { + _userManager.OnUserUpdated += OnUserUpdated; + + return Task.CompletedTask; + } + + /// + public Task StopAsync(CancellationToken cancellationToken) + { + _userManager.OnUserUpdated -= OnUserUpdated; + + return Task.CompletedTask; + } + + private async void OnUserUpdated(object? sender, GenericEventArgs e) + { + var user = e.Argument; + if (!user.HasPermission(PermissionKind.EnableAllDevices)) + { + await UpdateDeviceAccess(user).ConfigureAwait(false); + } + } + + private async Task UpdateDeviceAccess(User user) + { + var existing = (await _deviceManager.GetDevices(new DeviceQuery + { + UserId = user.Id + }).ConfigureAwait(false)).Items; + + foreach (var device in existing) + { + if (!string.IsNullOrEmpty(device.DeviceId) && !_deviceManager.CanAccessDevice(user, device.DeviceId)) + { + await _sessionManager.Logout(device).ConfigureAwait(false); + } + } + } +} From 4e02d8aa21eedc6fe9c1d3ee843db3d5e3858b4c Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Tue, 6 Feb 2024 15:40:52 -0500 Subject: [PATCH 3/9] Convert LibraryChangedNotifier to IHostedService --- .../EntryPoints/LibraryChangedNotifier.cs | 45 ++++++++++--------- Jellyfin.Server/Startup.cs | 2 + 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index 83e7b230d..4c668379c 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -13,19 +13,19 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Session; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.EntryPoints; /// -/// A that notifies users when libraries are updated. +/// A responsible for notifying users when libraries are updated. /// -public sealed class LibraryChangedNotifier : IServerEntryPoint +public sealed class LibraryChangedNotifier : IHostedService, IDisposable { private readonly ILibraryManager _libraryManager; private readonly IServerConfigurationManager _configurationManager; @@ -70,7 +70,7 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint } /// - public Task RunAsync() + public Task StartAsync(CancellationToken cancellationToken) { _libraryManager.ItemAdded += OnLibraryItemAdded; _libraryManager.ItemUpdated += OnLibraryItemUpdated; @@ -83,6 +83,20 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint return Task.CompletedTask; } + /// + public Task StopAsync(CancellationToken cancellationToken) + { + _libraryManager.ItemAdded -= OnLibraryItemAdded; + _libraryManager.ItemUpdated -= OnLibraryItemUpdated; + _libraryManager.ItemRemoved -= OnLibraryItemRemoved; + + _providerManager.RefreshCompleted -= OnProviderRefreshCompleted; + _providerManager.RefreshStarted -= OnProviderRefreshStarted; + _providerManager.RefreshProgress -= OnProviderRefreshProgress; + + return Task.CompletedTask; + } + private void OnProviderRefreshProgress(object? sender, GenericEventArgs> e) { var item = e.Argument.Item1; @@ -137,9 +151,7 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint } private void OnProviderRefreshStarted(object? sender, GenericEventArgs e) - { - OnProviderRefreshProgress(sender, new GenericEventArgs>(new Tuple(e.Argument, 0))); - } + => OnProviderRefreshProgress(sender, new GenericEventArgs>(new Tuple(e.Argument, 0))); private void OnProviderRefreshCompleted(object? sender, GenericEventArgs e) { @@ -342,7 +354,7 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint return item.SourceType == SourceType.Library; } - private IEnumerable GetTopParentIds(List items, List allUserRootChildren) + private static IEnumerable GetTopParentIds(List items, List allUserRootChildren) { var list = new List(); @@ -363,7 +375,7 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint return list.Distinct(StringComparer.Ordinal); } - private IEnumerable TranslatePhysicalItemToUserLibrary(T item, User user, bool includeIfNotFound = false) + private T[] TranslatePhysicalItemToUserLibrary(T item, User user, bool includeIfNotFound = false) where T : BaseItem { // If the physical root changed, return the user root @@ -384,18 +396,7 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint /// public void Dispose() { - _libraryManager.ItemAdded -= OnLibraryItemAdded; - _libraryManager.ItemUpdated -= OnLibraryItemUpdated; - _libraryManager.ItemRemoved -= OnLibraryItemRemoved; - - _providerManager.RefreshCompleted -= OnProviderRefreshCompleted; - _providerManager.RefreshStarted -= OnProviderRefreshStarted; - _providerManager.RefreshProgress -= OnProviderRefreshProgress; - - if (_libraryUpdateTimer is not null) - { - _libraryUpdateTimer.Dispose(); - _libraryUpdateTimer = null; - } + _libraryUpdateTimer?.Dispose(); + _libraryUpdateTimer = null; } } diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 7cf7d75da..bb5513f86 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -4,6 +4,7 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Net.Mime; using System.Text; +using Emby.Server.Implementations.EntryPoints; using Jellyfin.Api.Middleware; using Jellyfin.LiveTv.Extensions; using Jellyfin.MediaEncoding.Hls.Extensions; @@ -126,6 +127,7 @@ namespace Jellyfin.Server services.AddHostedService(); services.AddHostedService(); + services.AddHostedService(); } /// From 9e62b6919ffc7c24b66300279c2fd3c4a0c7f5bd Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Tue, 6 Feb 2024 15:54:03 -0500 Subject: [PATCH 4/9] Convert UserDataChangeNotifier to IHostedService --- .../EntryPoints/UserDataChangeNotifier.cs | 88 ++++++++++--------- Jellyfin.Server/Startup.cs | 1 + 2 files changed, 47 insertions(+), 42 deletions(-) diff --git a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs index d32759017..957ad9c01 100644 --- a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.Globalization; @@ -8,14 +6,17 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Session; +using Microsoft.Extensions.Hosting; namespace Emby.Server.Implementations.EntryPoints { - public sealed class UserDataChangeNotifier : IServerEntryPoint + /// + /// responsible for notifying users when associated item data is updated. + /// + public sealed class UserDataChangeNotifier : IHostedService, IDisposable { private const int UpdateDuration = 500; @@ -23,25 +24,43 @@ namespace Emby.Server.Implementations.EntryPoints private readonly IUserDataManager _userDataManager; private readonly IUserManager _userManager; - private readonly Dictionary> _changedItems = new Dictionary>(); + private readonly Dictionary> _changedItems = new(); + private readonly object _syncLock = new(); - private readonly object _syncLock = new object(); private Timer? _updateTimer; - public UserDataChangeNotifier(IUserDataManager userDataManager, ISessionManager sessionManager, IUserManager userManager) + /// + /// Initializes a new instance of the class. + /// + /// The . + /// The . + /// The . + public UserDataChangeNotifier( + IUserDataManager userDataManager, + ISessionManager sessionManager, + IUserManager userManager) { _userDataManager = userDataManager; _sessionManager = sessionManager; _userManager = userManager; } - public Task RunAsync() + /// + public Task StartAsync(CancellationToken cancellationToken) { _userDataManager.UserDataSaved += OnUserDataManagerUserDataSaved; return Task.CompletedTask; } + /// + public Task StopAsync(CancellationToken cancellationToken) + { + _userDataManager.UserDataSaved -= OnUserDataManagerUserDataSaved; + + return Task.CompletedTask; + } + private void OnUserDataManagerUserDataSaved(object? sender, UserDataSaveEventArgs e) { if (e.SaveReason == UserDataSaveReason.PlaybackProgress) @@ -103,55 +122,40 @@ namespace Emby.Server.Implementations.EntryPoints } } - await SendNotifications(changes, CancellationToken.None).ConfigureAwait(false); - } - - private async Task SendNotifications(List>> changes, CancellationToken cancellationToken) - { - foreach ((var key, var value) in changes) + foreach (var (userId, changedItems) in changes) { - await SendNotifications(key, value, cancellationToken).ConfigureAwait(false); + await _sessionManager.SendMessageToUserSessions( + [userId], + SessionMessageType.UserDataChanged, + () => GetUserDataChangeInfo(userId, changedItems), + default).ConfigureAwait(false); } } - private Task SendNotifications(Guid userId, List changedItems, CancellationToken cancellationToken) - { - return _sessionManager.SendMessageToUserSessions(new List { userId }, SessionMessageType.UserDataChanged, () => GetUserDataChangeInfo(userId, changedItems), cancellationToken); - } - private UserDataChangeInfo GetUserDataChangeInfo(Guid userId, List changedItems) { var user = _userManager.GetUserById(userId); - var dtoList = changedItems - .DistinctBy(x => x.Id) - .Select(i => - { - var dto = _userDataManager.GetUserDataDto(i, user); - dto.ItemId = i.Id.ToString("N", CultureInfo.InvariantCulture); - return dto; - }) - .ToArray(); - - var userIdString = userId.ToString("N", CultureInfo.InvariantCulture); - return new UserDataChangeInfo { - UserId = userIdString, - - UserDataList = dtoList + UserId = userId.ToString("N", CultureInfo.InvariantCulture), + UserDataList = changedItems + .DistinctBy(x => x.Id) + .Select(i => + { + var dto = _userDataManager.GetUserDataDto(i, user); + dto.ItemId = i.Id.ToString("N", CultureInfo.InvariantCulture); + return dto; + }) + .ToArray() }; } + /// public void Dispose() { - if (_updateTimer is not null) - { - _updateTimer.Dispose(); - _updateTimer = null; - } - - _userDataManager.UserDataSaved -= OnUserDataManagerUserDataSaved; + _updateTimer?.Dispose(); + _updateTimer = null; } } } diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index bb5513f86..b0bb182aa 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -128,6 +128,7 @@ namespace Jellyfin.Server services.AddHostedService(); services.AddHostedService(); services.AddHostedService(); + services.AddHostedService(); } /// From 24b4d025967135a8895fedf1c45f3679f3b89393 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Tue, 6 Feb 2024 16:08:41 -0500 Subject: [PATCH 5/9] Convert RecordingNotifier to IHostedService --- Jellyfin.Server/Startup.cs | 2 + src/Jellyfin.LiveTv/RecordingNotifier.cs | 73 ++++++++++++------------ 2 files changed, 40 insertions(+), 35 deletions(-) diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index b0bb182aa..84f9bff61 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -6,6 +6,7 @@ using System.Net.Mime; using System.Text; using Emby.Server.Implementations.EntryPoints; using Jellyfin.Api.Middleware; +using Jellyfin.LiveTv; using Jellyfin.LiveTv.Extensions; using Jellyfin.MediaEncoding.Hls.Extensions; using Jellyfin.Networking; @@ -129,6 +130,7 @@ namespace Jellyfin.Server services.AddHostedService(); services.AddHostedService(); services.AddHostedService(); + services.AddHostedService(); } /// diff --git a/src/Jellyfin.LiveTv/RecordingNotifier.cs b/src/Jellyfin.LiveTv/RecordingNotifier.cs index 2923948eb..226d525e7 100644 --- a/src/Jellyfin.LiveTv/RecordingNotifier.cs +++ b/src/Jellyfin.LiveTv/RecordingNotifier.cs @@ -1,7 +1,3 @@ -#nullable disable - -#pragma warning disable CS1591 - using System; using System.Linq; using System.Threading; @@ -10,34 +6,44 @@ using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Session; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace Jellyfin.LiveTv { - public sealed class RecordingNotifier : IServerEntryPoint + /// + /// responsible for notifying users when a LiveTV recording is completed. + /// + public sealed class RecordingNotifier : IHostedService { - private readonly ILiveTvManager _liveTvManager; + private readonly ILogger _logger; private readonly ISessionManager _sessionManager; private readonly IUserManager _userManager; - private readonly ILogger _logger; + private readonly ILiveTvManager _liveTvManager; + /// + /// Initializes a new instance of the class. + /// + /// The . + /// The . + /// The . + /// The . public RecordingNotifier( + ILogger logger, ISessionManager sessionManager, IUserManager userManager, - ILogger logger, ILiveTvManager liveTvManager) { + _logger = logger; _sessionManager = sessionManager; _userManager = userManager; - _logger = logger; _liveTvManager = liveTvManager; } /// - public Task RunAsync() + public Task StartAsync(CancellationToken cancellationToken) { _liveTvManager.TimerCancelled += OnLiveTvManagerTimerCancelled; _liveTvManager.SeriesTimerCancelled += OnLiveTvManagerSeriesTimerCancelled; @@ -47,29 +53,35 @@ namespace Jellyfin.LiveTv return Task.CompletedTask; } - private async void OnLiveTvManagerSeriesTimerCreated(object sender, GenericEventArgs e) + /// + public Task StopAsync(CancellationToken cancellationToken) { - await SendMessage(SessionMessageType.SeriesTimerCreated, e.Argument).ConfigureAwait(false); + _liveTvManager.TimerCancelled -= OnLiveTvManagerTimerCancelled; + _liveTvManager.SeriesTimerCancelled -= OnLiveTvManagerSeriesTimerCancelled; + _liveTvManager.TimerCreated -= OnLiveTvManagerTimerCreated; + _liveTvManager.SeriesTimerCreated -= OnLiveTvManagerSeriesTimerCreated; + + return Task.CompletedTask; } - private async void OnLiveTvManagerTimerCreated(object sender, GenericEventArgs e) - { - await SendMessage(SessionMessageType.TimerCreated, e.Argument).ConfigureAwait(false); - } + private async void OnLiveTvManagerSeriesTimerCreated(object? sender, GenericEventArgs e) + => await SendMessage(SessionMessageType.SeriesTimerCreated, e.Argument).ConfigureAwait(false); - private async void OnLiveTvManagerSeriesTimerCancelled(object sender, GenericEventArgs e) - { - await SendMessage(SessionMessageType.SeriesTimerCancelled, e.Argument).ConfigureAwait(false); - } + private async void OnLiveTvManagerTimerCreated(object? sender, GenericEventArgs e) + => await SendMessage(SessionMessageType.TimerCreated, e.Argument).ConfigureAwait(false); - private async void OnLiveTvManagerTimerCancelled(object sender, GenericEventArgs e) - { - await SendMessage(SessionMessageType.TimerCancelled, e.Argument).ConfigureAwait(false); - } + private async void OnLiveTvManagerSeriesTimerCancelled(object? sender, GenericEventArgs e) + => await SendMessage(SessionMessageType.SeriesTimerCancelled, e.Argument).ConfigureAwait(false); + + private async void OnLiveTvManagerTimerCancelled(object? sender, GenericEventArgs e) + => await SendMessage(SessionMessageType.TimerCancelled, e.Argument).ConfigureAwait(false); private async Task SendMessage(SessionMessageType name, TimerEventInfo info) { - var users = _userManager.Users.Where(i => i.HasPermission(PermissionKind.EnableLiveTvAccess)).Select(i => i.Id).ToList(); + var users = _userManager.Users + .Where(i => i.HasPermission(PermissionKind.EnableLiveTvAccess)) + .Select(i => i.Id) + .ToList(); try { @@ -80,14 +92,5 @@ namespace Jellyfin.LiveTv _logger.LogError(ex, "Error sending message"); } } - - /// - public void Dispose() - { - _liveTvManager.TimerCancelled -= OnLiveTvManagerTimerCancelled; - _liveTvManager.SeriesTimerCancelled -= OnLiveTvManagerSeriesTimerCancelled; - _liveTvManager.TimerCreated -= OnLiveTvManagerTimerCreated; - _liveTvManager.SeriesTimerCreated -= OnLiveTvManagerSeriesTimerCreated; - } } } From 690e603b90ae9d856386d5fb6bf3e32d4cb46a9c Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Tue, 6 Feb 2024 16:18:17 -0500 Subject: [PATCH 6/9] Use IHostedService for NFO user data --- Jellyfin.Server/Startup.cs | 2 + MediaBrowser.XbmcMetadata/EntryPoint.cs | 78 ----------------- MediaBrowser.XbmcMetadata/NfoUserDataSaver.cs | 87 +++++++++++++++++++ 3 files changed, 89 insertions(+), 78 deletions(-) delete mode 100644 MediaBrowser.XbmcMetadata/EntryPoint.cs create mode 100644 MediaBrowser.XbmcMetadata/NfoUserDataSaver.cs diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 84f9bff61..1d78b1602 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -19,6 +19,7 @@ using Jellyfin.Server.Infrastructure; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Extensions; +using MediaBrowser.XbmcMetadata; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; @@ -128,6 +129,7 @@ namespace Jellyfin.Server services.AddHostedService(); services.AddHostedService(); + services.AddHostedService(); services.AddHostedService(); services.AddHostedService(); services.AddHostedService(); diff --git a/MediaBrowser.XbmcMetadata/EntryPoint.cs b/MediaBrowser.XbmcMetadata/EntryPoint.cs deleted file mode 100644 index a6216ef30..000000000 --- a/MediaBrowser.XbmcMetadata/EntryPoint.cs +++ /dev/null @@ -1,78 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Plugins; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.XbmcMetadata.Configuration; -using MediaBrowser.XbmcMetadata.Savers; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.XbmcMetadata -{ - public sealed class EntryPoint : IServerEntryPoint - { - private readonly IUserDataManager _userDataManager; - private readonly ILogger _logger; - private readonly IProviderManager _providerManager; - private readonly IConfigurationManager _config; - - public EntryPoint( - IUserDataManager userDataManager, - ILogger logger, - IProviderManager providerManager, - IConfigurationManager config) - { - _userDataManager = userDataManager; - _logger = logger; - _providerManager = providerManager; - _config = config; - } - - /// - public Task RunAsync() - { - _userDataManager.UserDataSaved += OnUserDataSaved; - - return Task.CompletedTask; - } - - private void OnUserDataSaved(object? sender, UserDataSaveEventArgs e) - { - if (e.SaveReason == UserDataSaveReason.PlaybackFinished || e.SaveReason == UserDataSaveReason.TogglePlayed || e.SaveReason == UserDataSaveReason.UpdateUserRating) - { - if (!string.IsNullOrWhiteSpace(_config.GetNfoConfiguration().UserId)) - { - _ = SaveMetadataForItemAsync(e.Item, ItemUpdateType.MetadataDownload); - } - } - } - - /// - public void Dispose() - { - _userDataManager.UserDataSaved -= OnUserDataSaved; - } - - private async Task SaveMetadataForItemAsync(BaseItem item, ItemUpdateType updateReason) - { - if (!item.IsFileProtocol || !item.SupportsLocalMetadata) - { - return; - } - - try - { - await _providerManager.SaveMetadataAsync(item, updateReason, new[] { BaseNfoSaver.SaverName }).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error saving metadata for {Path}", item.Path ?? item.Name); - } - } - } -} diff --git a/MediaBrowser.XbmcMetadata/NfoUserDataSaver.cs b/MediaBrowser.XbmcMetadata/NfoUserDataSaver.cs new file mode 100644 index 000000000..b2882194d --- /dev/null +++ b/MediaBrowser.XbmcMetadata/NfoUserDataSaver.cs @@ -0,0 +1,87 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.XbmcMetadata.Configuration; +using MediaBrowser.XbmcMetadata.Savers; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.XbmcMetadata; + +/// +/// responsible for updating NFO files' user data. +/// +public sealed class NfoUserDataSaver : IHostedService +{ + private readonly ILogger _logger; + private readonly IConfigurationManager _config; + private readonly IUserDataManager _userDataManager; + private readonly IProviderManager _providerManager; + + /// + /// Initializes a new instance of the class. + /// + /// The . + /// The . + /// The . + /// The . + public NfoUserDataSaver( + ILogger logger, + IConfigurationManager config, + IUserDataManager userDataManager, + IProviderManager providerManager) + { + _logger = logger; + _config = config; + _userDataManager = userDataManager; + _providerManager = providerManager; + } + + /// + public Task StartAsync(CancellationToken cancellationToken) + { + _userDataManager.UserDataSaved += OnUserDataSaved; + return Task.CompletedTask; + } + + /// + public Task StopAsync(CancellationToken cancellationToken) + { + _userDataManager.UserDataSaved -= OnUserDataSaved; + return Task.CompletedTask; + } + + private async void OnUserDataSaved(object? sender, UserDataSaveEventArgs e) + { + if (e.SaveReason is not (UserDataSaveReason.PlaybackFinished + or UserDataSaveReason.TogglePlayed or UserDataSaveReason.UpdateUserRating)) + { + return; + } + + if (string.IsNullOrWhiteSpace(_config.GetNfoConfiguration().UserId)) + { + return; + } + + var item = e.Item; + if (!item.IsFileProtocol || !item.SupportsLocalMetadata) + { + return; + } + + try + { + await _providerManager.SaveMetadataAsync(item, ItemUpdateType.MetadataDownload, [BaseNfoSaver.SaverName]) + .ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error saving metadata for {Path}", item.Path ?? item.Name); + } + } +} From c9311c9e7e048eadb1abb62a1904098adb740a76 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Tue, 6 Feb 2024 16:23:18 -0500 Subject: [PATCH 7/9] Use IHostedService for Live TV --- Jellyfin.Server/Startup.cs | 2 ++ src/Jellyfin.LiveTv/EmbyTV/EntryPoint.cs | 21 ---------------- src/Jellyfin.LiveTv/EmbyTV/LiveTvHost.cs | 31 ++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 21 deletions(-) delete mode 100644 src/Jellyfin.LiveTv/EmbyTV/EntryPoint.cs create mode 100644 src/Jellyfin.LiveTv/EmbyTV/LiveTvHost.cs diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 1d78b1602..558ad5b7b 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -7,6 +7,7 @@ using System.Text; using Emby.Server.Implementations.EntryPoints; using Jellyfin.Api.Middleware; using Jellyfin.LiveTv; +using Jellyfin.LiveTv.EmbyTV; using Jellyfin.LiveTv.Extensions; using Jellyfin.MediaEncoding.Hls.Extensions; using Jellyfin.Networking; @@ -127,6 +128,7 @@ namespace Jellyfin.Server services.AddHlsPlaylistGenerator(); services.AddLiveTvServices(); + services.AddHostedService(); services.AddHostedService(); services.AddHostedService(); services.AddHostedService(); diff --git a/src/Jellyfin.LiveTv/EmbyTV/EntryPoint.cs b/src/Jellyfin.LiveTv/EmbyTV/EntryPoint.cs deleted file mode 100644 index e750c05ac..000000000 --- a/src/Jellyfin.LiveTv/EmbyTV/EntryPoint.cs +++ /dev/null @@ -1,21 +0,0 @@ -#pragma warning disable CS1591 - -using System.Threading.Tasks; -using MediaBrowser.Controller.Plugins; - -namespace Jellyfin.LiveTv.EmbyTV -{ - public sealed class EntryPoint : IServerEntryPoint - { - /// - public Task RunAsync() - { - return EmbyTV.Current.Start(); - } - - /// - public void Dispose() - { - } - } -} diff --git a/src/Jellyfin.LiveTv/EmbyTV/LiveTvHost.cs b/src/Jellyfin.LiveTv/EmbyTV/LiveTvHost.cs new file mode 100644 index 000000000..dc15d53ff --- /dev/null +++ b/src/Jellyfin.LiveTv/EmbyTV/LiveTvHost.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.LiveTv; +using Microsoft.Extensions.Hosting; + +namespace Jellyfin.LiveTv.EmbyTV; + +/// +/// responsible for initializing Live TV. +/// +public sealed class LiveTvHost : IHostedService +{ + private readonly EmbyTV _service; + + /// + /// Initializes a new instance of the class. + /// + /// The available s. + public LiveTvHost(IEnumerable services) + { + _service = services.OfType().First(); + } + + /// + public Task StartAsync(CancellationToken cancellationToken) => _service.Start(); + + /// + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; +} From 4c7eca931390f82237273b39cc26381323623180 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Tue, 6 Feb 2024 16:35:38 -0500 Subject: [PATCH 8/9] Use IHostApplicationLifetime to start library monitor --- .../IO/LibraryMonitor.cs | 73 ++++++------------- .../IO/LibraryMonitorStartup.cs | 35 --------- .../Library/ILibraryMonitor.cs | 9 +-- 3 files changed, 25 insertions(+), 92 deletions(-) delete mode 100644 Emby.Server.Implementations/IO/LibraryMonitorStartup.cs diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index dde38906f..31617d1a5 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -11,11 +9,13 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Model.IO; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.IO { - public class LibraryMonitor : ILibraryMonitor + /// + public sealed class LibraryMonitor : ILibraryMonitor, IDisposable { private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; @@ -25,19 +25,19 @@ namespace Emby.Server.Implementations.IO /// /// The file system watchers. /// - private readonly ConcurrentDictionary _fileSystemWatchers = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary _fileSystemWatchers = new(StringComparer.OrdinalIgnoreCase); /// /// The affected paths. /// - private readonly List _activeRefreshers = new List(); + private readonly List _activeRefreshers = []; /// /// A dynamic list of paths that should be ignored. Added to during our own file system modifications. /// - private readonly ConcurrentDictionary _tempIgnoredPaths = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary _tempIgnoredPaths = new(StringComparer.OrdinalIgnoreCase); - private bool _disposed = false; + private bool _disposed; /// /// Initializes a new instance of the class. @@ -46,34 +46,31 @@ namespace Emby.Server.Implementations.IO /// The library manager. /// The configuration manager. /// The filesystem. + /// The . public LibraryMonitor( ILogger logger, ILibraryManager libraryManager, IServerConfigurationManager configurationManager, - IFileSystem fileSystem) + IFileSystem fileSystem, + IHostApplicationLifetime appLifetime) { _libraryManager = libraryManager; _logger = logger; _configurationManager = configurationManager; _fileSystem = fileSystem; + + appLifetime.ApplicationStarted.Register(Start); } - /// - /// Add the path to our temporary ignore list. Use when writing to a path within our listening scope. - /// - /// The path. - private void TemporarilyIgnore(string path) - { - _tempIgnoredPaths[path] = path; - } - + /// public void ReportFileSystemChangeBeginning(string path) { ArgumentException.ThrowIfNullOrEmpty(path); - TemporarilyIgnore(path); + _tempIgnoredPaths[path] = path; } + /// public async void ReportFileSystemChangeComplete(string path, bool refreshPath) { ArgumentException.ThrowIfNullOrEmpty(path); @@ -107,14 +104,10 @@ namespace Emby.Server.Implementations.IO var options = _libraryManager.GetLibraryOptions(item); - if (options is not null) - { - return options.EnableRealtimeMonitor; - } - - return false; + return options is not null && options.EnableRealtimeMonitor; } + /// public void Start() { _libraryManager.ItemAdded += OnLibraryManagerItemAdded; @@ -306,20 +299,11 @@ namespace Emby.Server.Implementations.IO { if (removeFromList) { - RemoveWatcherFromList(watcher); + _fileSystemWatchers.TryRemove(watcher.Path, out _); } } } - /// - /// Removes the watcher from list. - /// - /// The watcher. - private void RemoveWatcherFromList(FileSystemWatcher watcher) - { - _fileSystemWatchers.TryRemove(watcher.Path, out _); - } - /// /// Handles the Error event of the watcher control. /// @@ -352,6 +336,7 @@ namespace Emby.Server.Implementations.IO } } + /// public void ReportFileSystemChanged(string path) { ArgumentException.ThrowIfNullOrEmpty(path); @@ -479,31 +464,15 @@ namespace Emby.Server.Implementations.IO } } - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// + /// public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Releases unmanaged and - optionally - managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected virtual void Dispose(bool disposing) { if (_disposed) { return; } - if (disposing) - { - Stop(); - } - + Stop(); _disposed = true; } } diff --git a/Emby.Server.Implementations/IO/LibraryMonitorStartup.cs b/Emby.Server.Implementations/IO/LibraryMonitorStartup.cs deleted file mode 100644 index c51cf0545..000000000 --- a/Emby.Server.Implementations/IO/LibraryMonitorStartup.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Threading.Tasks; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Plugins; - -namespace Emby.Server.Implementations.IO -{ - /// - /// which is responsible for starting the library monitor. - /// - public sealed class LibraryMonitorStartup : IServerEntryPoint - { - private readonly ILibraryMonitor _monitor; - - /// - /// Initializes a new instance of the class. - /// - /// The library monitor. - public LibraryMonitorStartup(ILibraryMonitor monitor) - { - _monitor = monitor; - } - - /// - public Task RunAsync() - { - _monitor.Start(); - return Task.CompletedTask; - } - - /// - public void Dispose() - { - } - } -} diff --git a/MediaBrowser.Controller/Library/ILibraryMonitor.cs b/MediaBrowser.Controller/Library/ILibraryMonitor.cs index de74aa5a1..6d2f5b873 100644 --- a/MediaBrowser.Controller/Library/ILibraryMonitor.cs +++ b/MediaBrowser.Controller/Library/ILibraryMonitor.cs @@ -1,10 +1,9 @@ -#pragma warning disable CS1591 - -using System; - namespace MediaBrowser.Controller.Library { - public interface ILibraryMonitor : IDisposable + /// + /// Service responsible for monitoring library filesystems for changes. + /// + public interface ILibraryMonitor { /// /// Starts this instance. From 19a72e8bf2b1a68fddb992357577683027408e90 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Tue, 6 Feb 2024 16:38:12 -0500 Subject: [PATCH 9/9] Remove IServerEntryPoint --- .../ApplicationHost.cs | 33 ++----------------- .../Plugins/IRunBeforeStartup.cs | 9 ----- .../Plugins/IServerEntryPoint.cs | 20 ----------- 3 files changed, 2 insertions(+), 60 deletions(-) delete mode 100644 MediaBrowser.Controller/Plugins/IRunBeforeStartup.cs delete mode 100644 MediaBrowser.Controller/Plugins/IServerEntryPoint.cs diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index d268a6ba8..550c16b4c 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -62,7 +62,6 @@ using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Playlists; -using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.QuickConnect; using MediaBrowser.Controller.Resolvers; @@ -393,7 +392,7 @@ namespace Emby.Server.Implementations /// Runs the startup tasks. /// /// . - public async Task RunStartupTasksAsync() + public Task RunStartupTasksAsync() { Logger.LogInformation("Running startup tasks"); @@ -405,38 +404,10 @@ namespace Emby.Server.Implementations Resolve().SetFFmpegPath(); Logger.LogInformation("ServerId: {ServerId}", SystemId); - - var entryPoints = GetExports(); - - var stopWatch = new Stopwatch(); - stopWatch.Start(); - - await Task.WhenAll(StartEntryPoints(entryPoints, true)).ConfigureAwait(false); - Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed); - Logger.LogInformation("Core startup complete"); CoreStartupHasCompleted = true; - stopWatch.Restart(); - - await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false); - Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed); - stopWatch.Stop(); - } - - private IEnumerable StartEntryPoints(IEnumerable entryPoints, bool isBeforeStartup) - { - foreach (var entryPoint in entryPoints) - { - if (isBeforeStartup != (entryPoint is IRunBeforeStartup)) - { - continue; - } - - Logger.LogDebug("Starting entry point {Type}", entryPoint.GetType()); - - yield return entryPoint.RunAsync(); - } + return Task.CompletedTask; } /// diff --git a/MediaBrowser.Controller/Plugins/IRunBeforeStartup.cs b/MediaBrowser.Controller/Plugins/IRunBeforeStartup.cs deleted file mode 100644 index 2b831103a..000000000 --- a/MediaBrowser.Controller/Plugins/IRunBeforeStartup.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace MediaBrowser.Controller.Plugins -{ - /// - /// Indicates that a should be invoked as a pre-startup task. - /// - public interface IRunBeforeStartup - { - } -} diff --git a/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs b/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs deleted file mode 100644 index 6024661e1..000000000 --- a/MediaBrowser.Controller/Plugins/IServerEntryPoint.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace MediaBrowser.Controller.Plugins -{ - /// - /// Represents an entry point for a module in the application. This interface is scanned for automatically and - /// provides a hook to initialize the module at application start. - /// The entry point can additionally be flagged as a pre-startup task by implementing the - /// interface. - /// - public interface IServerEntryPoint : IDisposable - { - /// - /// Run the initialization for this module. This method is invoked at application start. - /// - /// A representing the asynchronous operation. - Task RunAsync(); - } -}