@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System ;
using System.Collections.Concurrent ;
using System.Collections.Generic ;
using System.Diagnostics ;
using System.Globalization ;
@ -8,6 +9,7 @@ using System.IO;
using System.Linq ;
using System.Net ;
using System.Net.Http ;
using System.Net.Sockets ;
using System.Reflection ;
using System.Runtime.InteropServices ;
using System.Security.Cryptography.X509Certificates ;
@ -15,7 +17,8 @@ using System.Text;
using System.Threading ;
using System.Threading.Tasks ;
using Emby.Dlna ;
using Emby.Dlna.Common ;
using Emby.Dlna.Main ;
using Emby.Dlna.Ssdp ;
using Emby.Drawing ;
using Emby.Notifications ;
using Emby.Photos ;
@ -27,11 +30,13 @@ using Emby.Server.Implementations.Cryptography;
using Emby.Server.Implementations.Data ;
using Emby.Server.Implementations.Devices ;
using Emby.Server.Implementations.Dto ;
using Emby.Server.Implementations.HttpServer ;
using Emby.Server.Implementations.HttpServer.Security ;
using Emby.Server.Implementations.IO ;
using Emby.Server.Implementations.Library ;
using Emby.Server.Implementations.LiveTv ;
using Emby.Server.Implementations.Localization ;
using Emby.Server.Implementations.Net ;
using Emby.Server.Implementations.Playlists ;
using Emby.Server.Implementations.Plugins ;
using Emby.Server.Implementations.QuickConnect ;
@ -43,13 +48,10 @@ using Emby.Server.Implementations.SyncPlay;
using Emby.Server.Implementations.TV ;
using Emby.Server.Implementations.Updates ;
using Jellyfin.Api.Helpers ;
using Jellyfin.Networking.Advertising ;
using Jellyfin.Networking.Gateway ;
using Jellyfin.Networking.Manager ;
using Jellyfin.Networking.UPnP ;
using MediaBrowser.Common ;
using MediaBrowser.Common.Configuration ;
using MediaBrowser.Common.Events ;
using MediaBrowser.Common.Json ;
using MediaBrowser.Common.Net ;
using MediaBrowser.Common.Plugins ;
using MediaBrowser.Common.Updates ;
@ -59,6 +61,7 @@ using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Collections ;
using MediaBrowser.Controller.Configuration ;
using MediaBrowser.Controller.Devices ;
using MediaBrowser.Controller.Dlna ;
using MediaBrowser.Controller.Drawing ;
using MediaBrowser.Controller.Dto ;
using MediaBrowser.Controller.Entities ;
@ -83,9 +86,11 @@ using MediaBrowser.LocalMetadata.Savers;
using MediaBrowser.MediaEncoding.BdInfo ;
using MediaBrowser.Model.Configuration ;
using MediaBrowser.Model.Cryptography ;
using MediaBrowser.Model.Dlna ;
using MediaBrowser.Model.Globalization ;
using MediaBrowser.Model.IO ;
using MediaBrowser.Model.MediaInfo ;
using MediaBrowser.Model.Net ;
using MediaBrowser.Model.Serialization ;
using MediaBrowser.Model.System ;
using MediaBrowser.Model.Tasks ;
@ -94,7 +99,6 @@ using MediaBrowser.Providers.Manager;
using MediaBrowser.Providers.Plugins.TheTvdb ;
using MediaBrowser.Providers.Subtitles ;
using MediaBrowser.XbmcMetadata.Providers ;
using Microsoft.AspNetCore.Http ;
using Microsoft.AspNetCore.Mvc ;
using Microsoft.Extensions.DependencyInjection ;
using Microsoft.Extensions.Logging ;
@ -115,6 +119,7 @@ namespace Emby.Server.Implementations
private static readonly string [ ] _relevantEnvVarPrefixes = { "JELLYFIN_" , "DOTNET_" , "ASPNETCORE_" } ;
private readonly IFileSystem _fileSystemManager ;
private readonly INetworkManager _networkManager ;
private readonly IXmlSerializer _xmlSerializer ;
private readonly IJsonSerializer _jsonSerializer ;
private readonly IStartupOptions _startupOptions ;
@ -209,7 +214,7 @@ namespace Emby.Server.Implementations
private readonly List < IDisposable > _disposableParts = new List < IDisposable > ( ) ;
/// <summary>
/// Gets or sets the configuration manager.
/// Gets the configuration manager.
/// </summary>
/// <value>The configuration manager.</value>
protected IConfigurationManager ConfigurationManager { get ; set ; }
@ -242,12 +247,14 @@ namespace Emby.Server.Implementations
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
/// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
public ApplicationHost (
IServerApplicationPaths applicationPaths ,
ILoggerFactory loggerFactory ,
IStartupOptions options ,
IFileSystem fileSystem ,
INetworkManager networkManager ,
IServiceCollection serviceCollection )
{
_xmlSerializer = new MyXmlSerializer ( ) ;
@ -255,14 +262,15 @@ namespace Emby.Server.Implementations
ServiceCollection = serviceCollection ;
_networkManager = networkManager ;
networkManager . LocalSubnetsFn = GetConfiguredLocalSubnets ;
ApplicationPaths = applicationPaths ;
LoggerFactory = loggerFactory ;
_fileSystemManager = fileSystem ;
ConfigurationManager = new ServerConfigurationManager ( ApplicationPaths , LoggerFactory , _xmlSerializer , _fileSystemManager ) ;
NetManager = new NetworkManager ( ( IServerConfigurationManager ) ConfigurationManager , LoggerFactory . CreateLogger < NetworkManager > ( ) ) ;
Logger = LoggerFactory . CreateLogger < ApplicationHost > ( ) ;
_startupOptions = options ;
@ -275,18 +283,19 @@ namespace Emby.Server.Implementations
fileSystem . AddShortcutHandler ( new MbLinkShortcutHandler ( fileSystem ) ) ;
_networkManager . NetworkChanged + = OnNetworkChanged ;
CertificateInfo = new CertificateInfo
{
Path = ServerConfigurationManager . Configuration . CertificatePath ,
Password = ServerConfigurationManager . Configuration . CertificatePassword
} ;
Certificate = GetCertificate ( CertificateInfo ) ;
}
/// <summary>
/// Gets the NetworkManager instance.
/// </summary>
public INetworkManager NetManager { get ; internal set ; }
ApplicationVersion = typeof ( ApplicationHost ) . Assembly . GetName ( ) . Version ;
ApplicationVersionString = ApplicationVersion . ToString ( 3 ) ;
ApplicationUserAgent = Name . Replace ( ' ' , '-' ) + "/" + ApplicationVersionString ;
}
public string ExpandVirtualPath ( string path )
{
@ -304,17 +313,27 @@ namespace Emby.Server.Implementations
. Replace ( appPaths . InternalMetadataPath , appPaths . VirtualInternalMetadataPath , StringComparison . OrdinalIgnoreCase ) ;
}
private string [ ] GetConfiguredLocalSubnets ( )
{
return ServerConfigurationManager . Configuration . LocalNetworkSubnets ;
}
private void OnNetworkChanged ( object sender , EventArgs e )
{
_validAddressResults . Clear ( ) ;
}
/// <inheritdoc />
public Version ApplicationVersion { get ; } = typeof ( ApplicationHost ) . Assembly . GetName ( ) . Version ;
public Version ApplicationVersion { get ; }
/// <inheritdoc />
public string ApplicationVersionString { get ; } = typeof ( ApplicationHost ) . Assembly . GetName ( ) . Version . ToString ( 3 ) ;
public string ApplicationVersionString { get ; }
/// <summary>
/// Gets the current application user agent.
/// </summary>
/// <value>The application user agent.</value>
public string ApplicationUserAgent => Name . Replace ( ' ' , '-' ) + "/" + ApplicationVersionString ;
public string ApplicationUserAgent { get ; }
/// <summary>
/// Gets the email address for use within a comment section of a user agent field.
@ -384,7 +403,7 @@ namespace Emby.Server.Implementations
/// <summary>
/// Resolves this instance.
/// </summary>
/// <typeparam name="T">The type . </typeparam>
/// <typeparam name="T">The type </typeparam>
/// <returns>``0.</returns>
public T Resolve < T > ( ) = > ServiceProvider . GetService < T > ( ) ;
@ -480,6 +499,21 @@ namespace Emby.Server.Implementations
HttpsPort = ServerConfiguration . DefaultHttpsPort ;
}
if ( Plugins ! = null )
{
var pluginBuilder = new StringBuilder ( ) ;
foreach ( var plugin in Plugins )
{
pluginBuilder . Append ( plugin . Name )
. Append ( ' ' )
. Append ( plugin . Version )
. AppendLine ( ) ;
}
Logger . LogInformation ( "Plugins: {Plugins}" , pluginBuilder . ToString ( ) ) ;
}
DiscoverTypes ( ) ;
RegisterServices ( ) ;
@ -504,10 +538,7 @@ namespace Emby.Server.Implementations
ServiceCollection . AddSingleton ( _fileSystemManager ) ;
ServiceCollection . AddSingleton < TvdbClientManager > ( ) ;
ServiceCollection . AddSingleton ( NetManager ) ;
ServiceCollection . AddSingleton < GatewayMonitor > ( ) ;
ServiceCollection . AddSingleton < WhoIsJellyfinServer > ( ) ;
ServiceCollection . AddSingleton < ExternalPortForwarding > ( ) ;
ServiceCollection . AddSingleton ( _networkManager ) ;
ServiceCollection . AddSingleton < IIsoManager , IsoManager > ( ) ;
@ -519,6 +550,8 @@ namespace Emby.Server.Implementations
ServiceCollection . AddSingleton < ICryptoProvider , CryptographyProvider > ( ) ;
ServiceCollection . AddSingleton < ISocketFactory , SocketFactory > ( ) ;
ServiceCollection . AddSingleton < IInstallationManager , InstallationManager > ( ) ;
ServiceCollection . AddSingleton < IZipClient , ZipClient > ( ) ;
@ -595,6 +628,8 @@ namespace Emby.Server.Implementations
ServiceCollection . AddSingleton < INotificationManager , NotificationManager > ( ) ;
ServiceCollection . AddSingleton < IDeviceDiscovery , DeviceDiscovery > ( ) ;
ServiceCollection . AddSingleton < IChapterManager , ChapterManager > ( ) ;
ServiceCollection . AddSingleton < IEncodingManager , MediaEncoder . EncodingManager > ( ) ;
@ -750,21 +785,6 @@ namespace Emby.Server.Implementations
. Where ( i = > i ! = null )
. ToArray ( ) ;
if ( Plugins ! = null )
{
var pluginBuilder = new StringBuilder ( ) ;
foreach ( var plugin in Plugins )
{
pluginBuilder . Append ( plugin . Name )
. Append ( ' ' )
. Append ( plugin . Version )
. AppendLine ( ) ;
}
Logger . LogInformation ( "Plugins: {Plugins}" , pluginBuilder . ToString ( ) ) ;
}
_urlPrefixes = GetUrlPrefixes ( ) . ToArray ( ) ;
_webSocketManager . Init ( GetExports < IWebSocketListener > ( ) ) ;
@ -799,6 +819,38 @@ namespace Emby.Server.Implementations
{
try
{
if ( plugin is IPluginAssembly assemblyPlugin )
{
var assembly = plugin . GetType ( ) . Assembly ;
var assemblyName = assembly . GetName ( ) ;
var assemblyFilePath = assembly . Location ;
var dataFolderPath = Path . Combine ( ApplicationPaths . PluginsPath , Path . GetFileNameWithoutExtension ( assemblyFilePath ) ) ;
assemblyPlugin . SetAttributes ( assemblyFilePath , dataFolderPath , assemblyName . Version ) ;
try
{
var idAttributes = assembly . GetCustomAttributes ( typeof ( GuidAttribute ) , true ) ;
if ( idAttributes . Length > 0 )
{
var attribute = ( GuidAttribute ) idAttributes [ 0 ] ;
var assemblyId = new Guid ( attribute . Value ) ;
assemblyPlugin . SetId ( assemblyId ) ;
}
}
catch ( Exception ex )
{
Logger . LogError ( ex , "Error getting plugin Id from {PluginName}." , plugin . GetType ( ) . FullName ) ;
}
}
if ( plugin is IHasPluginConfiguration hasPluginConfiguration )
{
hasPluginConfiguration . SetStartupInfo ( s = > Directory . CreateDirectory ( s ) ) ;
}
plugin . RegisterServices ( ServiceCollection ) ;
}
catch ( Exception ex )
@ -1149,6 +1201,9 @@ namespace Emby.Server.Implementations
// MediaEncoding
yield return typeof ( MediaBrowser . MediaEncoding . Encoder . MediaEncoder ) . Assembly ;
// Dlna
yield return typeof ( DlnaEntryPoint ) . Assembly ;
// Local metadata
yield return typeof ( BoxSetXmlSaver ) . Assembly ;
@ -1169,10 +1224,13 @@ namespace Emby.Server.Implementations
/// <summary>
/// Gets the system status.
/// </summary>
/// <param name=" source">Where this request originated .</param>
/// <param name=" cancellationToken">The cancellation token .</param>
/// <returns>SystemInfo.</returns>
public SystemInfo GetSystemInfo ( IPAddress source )
public async Task < SystemInfo > GetSystemInfo ( CancellationToken cancellationToken )
{
var localAddress = await GetLocalApiUrl ( cancellationToken ) . ConfigureAwait ( false ) ;
var transcodingTempPath = ConfigurationManager . GetTranscodePath ( ) ;
return new SystemInfo
{
HasPendingRestart = HasPendingRestart ,
@ -1192,9 +1250,9 @@ namespace Emby.Server.Implementations
CanSelfRestart = CanSelfRestart ,
CanLaunchWebBrowser = CanLaunchWebBrowser ,
HasUpdateAvailable = HasUpdateAvailable ,
TranscodingTempPath = Configura tionManager. GetT ranscodePath( ) ,
TranscodingTempPath = transcodingT emp Path,
ServerName = FriendlyName ,
LocalAddress = GetSmartApiUrl( source ) ,
LocalAddress = localAddress ,
SupportsLibraryMonitor = true ,
EncoderLocation = _mediaEncoder . EncoderLocation ,
SystemArchitecture = RuntimeInformation . OSArchitecture ,
@ -1203,12 +1261,14 @@ namespace Emby.Server.Implementations
}
public IEnumerable < WakeOnLanInfo > GetWakeOnLanInfo ( )
= > Net Manager. GetMacAddresses ( )
= > _network Manager. GetMacAddresses ( )
. Select ( i = > new WakeOnLanInfo ( i ) )
. ToList ( ) ;
public PublicSystemInfo GetPublicSystemInfo ( IPAddress source )
public async Task < PublicSystemInfo > GetPublicSystemInfo ( CancellationToken cancellationToken )
{
var localAddress = await GetLocalApiUrl ( cancellationToken ) . ConfigureAwait ( false ) ;
return new PublicSystemInfo
{
Version = ApplicationVersionString ,
@ -1216,7 +1276,7 @@ namespace Emby.Server.Implementations
Id = SystemId ,
OperatingSystem = OperatingSystem . Id . ToString ( ) ,
ServerName = FriendlyName ,
LocalAddress = GetSmartApiUrl( source ) ,
LocalAddress = localAddress ,
StartupWizardCompleted = ConfigurationManager . CommonConfiguration . IsStartupWizardCompleted
} ;
}
@ -1225,92 +1285,186 @@ namespace Emby.Server.Implementations
public bool ListenWithHttps = > Certificate ! = null & & ServerConfigurationManager . Configuration . EnableHttps ;
/// <inheritdoc/>
public string GetSmartApiUrl ( IPAddress ipAddress , int? port = null )
public async Task < string > GetLocalApiUrl ( CancellationToken cancellationToken )
{
// Published server ends with a /
if ( _startupOptions . PublishedServerUrl ! = null )
try
{
// Published server ends with a '/', so we need to remove it.
return _startupOptions . PublishedServerUrl . ToString ( ) . Trim ( '/' ) ;
}
// Return the first matched address, if found, or the first known local address
var addresses = await GetLocalIpAddressesInternal ( false , 1 , cancellationToken ) . ConfigureAwait ( false ) ;
if ( addresses . Count = = 0 )
{
return null ;
}
string smart = NetManager . GetBindInterface ( ipAddress , out port ) ;
// If the smartAPI doesn't start with http then treat it as a host or ip.
if ( smart . StartsWith ( "http" , StringComparison . OrdinalIgnoreCase ) )
return GetLocalApiUrl ( addresses [ 0 ] ) ;
}
catch ( Exception ex )
{
return smart . Trim ( '/' ) ;
Logger . LogError ( ex , "Error getting local Ip address information" ) ;
}
return GetLocalApiUrl ( smart . Trim ( '/' ) , null , port ) ;
return null ;
}
public string GetSmartApiUrl ( HttpRequest request , int? port = null )
/// <summary>
/// Removes the scope id from IPv6 addresses.
/// </summary>
/// <param name="address">The IPv6 address.</param>
/// <returns>The IPv6 address without the scope id.</returns>
private ReadOnlySpan < char > RemoveScopeId ( ReadOnlySpan < char > address )
{
// Published server ends with a /
if ( _startupOptions . PublishedServerUrl ! = null )
{
// Published server ends with a '/', so we need to remove it.
return _startupOptions . PublishedServerUrl . ToString ( ) . Trim ( '/' ) ;
}
string smart = NetManager . GetBindInterface ( request , out port ) ;
// If the smartAPI doesn't start with http then treat it as a host or ip.
if ( smart . StartsWith ( "http" , StringComparison . OrdinalIgnoreCase ) )
var index = address . IndexOf ( '%' ) ;
if ( index = = - 1 )
{
return smart. Trim ( '/' ) ;
return address ;
}
return GetLocalApiUrl( smart . Trim ( '/' ) , request . Scheme , port ) ;
return address . Slice ( 0 , index ) ;
}
public string GetSmartApiUrl ( string hostname , int? port = null )
/// <inheritdoc />
public string GetLocalApiUrl ( IPAddress ipAddress )
{
// Published server ends with a /
if ( _startupOptions . PublishedServerUrl ! = null )
if ( ipAddress . AddressFamily = = AddressFamily . InterNetworkV6 )
{
// Published server ends with a '/', so we need to remove it.
return _startupOptions . PublishedServerUrl . ToString ( ) . Trim ( '/' ) ;
}
string smart = NetManager . GetBindInterface ( hostname , out port ) ;
var str = RemoveScopeId ( ipAddress . ToString ( ) ) ;
Span < char > span = new char [ str . Length + 2 ] ;
span [ 0 ] = '[' ;
str . CopyTo ( span . Slice ( 1 ) ) ;
span [ ^ 1 ] = ']' ;
// If the smartAPI doesn't start with http then treat it as a host or ip.
if ( smart . StartsWith ( "http" , StringComparison . OrdinalIgnoreCase ) )
{
return smart . Trim ( '/' ) ;
return GetLocalApiUrl ( span ) ;
}
return GetLocalApiUrl ( smart. Trim ( '/' ) , null , port ) ;
return GetLocalApiUrl ( ipAddress . ToString ( ) ) ;
}
/// <inheritdoc/>
public string GetLoopbackHttpApiUrl ( )
{
if ( NetManager . IsIP6Enabled )
{
return GetLocalApiUrl ( "::1" , Uri . UriSchemeHttp , HttpPort ) ;
}
return GetLocalApiUrl ( "127.0.0.1" , Uri . UriSchemeHttp , HttpPort ) ;
}
/// <inheritdoc/>
public string GetLocalApiUrl ( string host , string scheme = null , int? port = null )
public string GetLocalApiUrl ( ReadOnlySpan < char > host , string scheme = null , int? port = null )
{
// NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does
// not. For consistency, always trim the trailing slash.
return new UriBuilder
{
Scheme = scheme ? ? ( ListenWithHttps ? Uri . UriSchemeHttps : Uri . UriSchemeHttp ) ,
Host = host ,
Host = host .ToString ( ) ,
Port = port ? ? ( ListenWithHttps ? HttpsPort : HttpPort ) ,
Path = ServerConfigurationManager . Configuration . BaseUrl
} . ToString ( ) . TrimEnd ( '/' ) ;
}
/// <summary>
/// Gets the servers friendly name.
/// </summary>
public Task < List < IPAddress > > GetLocalIpAddresses ( CancellationToken cancellationToken )
{
return GetLocalIpAddressesInternal ( true , 0 , cancellationToken ) ;
}
private async Task < List < IPAddress > > GetLocalIpAddressesInternal ( bool allowLoopback , int limit , CancellationToken cancellationToken )
{
var addresses = ServerConfigurationManager
. Configuration
. LocalNetworkAddresses
. Select ( x = > NormalizeConfiguredLocalAddress ( x ) )
. Where ( i = > i ! = null )
. ToList ( ) ;
if ( addresses . Count = = 0 )
{
addresses . AddRange ( _networkManager . GetLocalIpAddresses ( ) ) ;
}
var resultList = new List < IPAddress > ( ) ;
foreach ( var address in addresses )
{
if ( ! allowLoopback )
{
if ( address . Equals ( IPAddress . Loopback ) | | address . Equals ( IPAddress . IPv6Loopback ) )
{
continue ;
}
}
if ( await IsLocalIpAddressValidAsync ( address , cancellationToken ) . ConfigureAwait ( false ) )
{
resultList . Add ( address ) ;
if ( limit > 0 & & resultList . Count > = limit )
{
return resultList ;
}
}
}
return resultList ;
}
public IPAddress NormalizeConfiguredLocalAddress ( ReadOnlySpan < char > address )
{
var index = address . Trim ( '/' ) . IndexOf ( '/' ) ;
if ( index ! = - 1 )
{
address = address . Slice ( index + 1 ) ;
}
if ( IPAddress . TryParse ( address . Trim ( '/' ) , out IPAddress result ) )
{
return result ;
}
return null ;
}
private readonly ConcurrentDictionary < string , bool > _validAddressResults = new ConcurrentDictionary < string , bool > ( StringComparer . OrdinalIgnoreCase ) ;
private async Task < bool > IsLocalIpAddressValidAsync ( IPAddress address , CancellationToken cancellationToken )
{
if ( address . Equals ( IPAddress . Loopback )
| | address . Equals ( IPAddress . IPv6Loopback ) )
{
return true ;
}
var apiUrl = GetLocalApiUrl ( address ) + "/system/ping" ;
if ( _validAddressResults . TryGetValue ( apiUrl , out var cachedResult ) )
{
return cachedResult ;
}
try
{
using var request = new HttpRequestMessage ( HttpMethod . Post , apiUrl ) ;
using var response = await _httpClientFactory . CreateClient ( NamedClient . Default )
. SendAsync ( request , HttpCompletionOption . ResponseHeadersRead , cancellationToken ) . ConfigureAwait ( false ) ;
await using var stream = await response . Content . ReadAsStreamAsync ( ) . ConfigureAwait ( false ) ;
var result = await System . Text . Json . JsonSerializer . DeserializeAsync < string > ( stream , JsonDefaults . GetOptions ( ) , cancellationToken ) . ConfigureAwait ( false ) ;
var valid = string . Equals ( Name , result , StringComparison . OrdinalIgnoreCase ) ;
_validAddressResults . AddOrUpdate ( apiUrl , valid , ( k , v ) = > valid ) ;
Logger . LogDebug ( "Ping test result to {0}. Success: {1}" , apiUrl , valid ) ;
return valid ;
}
catch ( OperationCanceledException )
{
Logger . LogDebug ( "Ping test result to {0}. Success: {1}" , apiUrl , "Cancelled" ) ;
throw ;
}
catch ( Exception ex )
{
Logger . LogDebug ( ex , "Ping test result to {0}. Success: {1}" , apiUrl , false ) ;
_validAddressResults . AddOrUpdate ( apiUrl , false , ( k , v ) = > false ) ;
return false ;
}
}
public string FriendlyName = >
string . IsNullOrEmpty ( ServerConfigurationManager . Configuration . ServerName )
? Environment . MachineName
@ -1382,7 +1536,7 @@ namespace Emby.Server.Implementations
foreach ( var assembly in assemblies )
{
Logger . LogDebug ( "Found API endpoints in plugin { n ame}", assembly . FullName ) ;
Logger . LogDebug ( "Found API endpoints in plugin { N ame}", assembly . FullName ) ;
yield return assembly ;
}
}