make dlna project portable

This commit is contained in:
Luke Pulverenti
2016-11-04 04:31:05 -04:00
parent c29394a81a
commit 6d250c4050
54 changed files with 1324 additions and 1042 deletions

View File

@@ -1,33 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Rssdp.Infrastructure
{
/// <summary>
/// Implemented by components that can create a platform specific UDP socket implementation, and wrap it in the cross platform <see cref="IUdpSocket"/> interface.
/// </summary>
public interface ISocketFactory
{
/// <summary>
/// Createa a new unicast socket using the specified local port number.
/// </summary>
/// <param name="localPort">The local port to bind to.</param>
/// <returns>A <see cref="IUdpSocket"/> implementation.</returns>
IUdpSocket CreateUdpSocket(int localPort);
/// <summary>
/// Createa a new multicast socket using the specified multicast IP address, multicast time to live and local port.
/// </summary>
/// <param name="ipAddress">The multicast IP address to bind to.</param>
/// <param name="multicastTimeToLive">The multicast time to live value. Actually a maximum number of network hops for UDP packets.</param>
/// <param name="localPort">The local port to bind to.</param>
/// <returns>A <see cref="IUdpSocket"/> implementation.</returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "ip", Justification="IP is a well known and understood abbreviation and the full name is excessive.")]
IUdpSocket CreateUdpMulticastSocket(string ipAddress, int multicastTimeToLive, int localPort);
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
using MediaBrowser.Model.Net;
namespace Rssdp.Infrastructure
{
@@ -44,8 +45,8 @@ namespace Rssdp.Infrastructure
/// Sends a message to a particular address (uni or multicast) and port.
/// </summary>
/// <param name="messageData">A byte array containing the data to send.</param>
/// <param name="destination">A <see cref="UdpEndPoint"/> representing the destination address for the data. Can be either a multicast or unicast destination.</param>
Task SendMessage(byte[] messageData, UdpEndPoint destination);
/// <param name="destination">A <see cref="IpEndPointInfo"/> representing the destination address for the data. Can be either a multicast or unicast destination.</param>
Task SendMessage(byte[] messageData, IpEndPointInfo destination);
/// <summary>
/// Sends a message to the SSDP multicast address and port.

View File

@@ -1,28 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Rssdp.Infrastructure
{
/// <summary>
/// Provides a common interface across platforms for UDP sockets used by this SSDP implementation.
/// </summary>
public interface IUdpSocket : IDisposable
{
/// <summary>
/// Waits for and returns the next UDP message sent to this socket (uni or multicast).
/// </summary>
/// <returns></returns>
System.Threading.Tasks.Task<ReceivedUdpData> ReceiveAsync();
/// <summary>
/// Sends a UDP message to a particular end point (uni or multicast).
/// </summary>
/// <param name="messageData">The data to send.</param>
/// <param name="endPoint">The <see cref="UdpEndPoint"/> providing the address and port to send to.</param>
Task SendTo(byte[] messageData, UdpEndPoint endPoint);
}
}

View File

@@ -1,19 +1,30 @@
using System.Reflection;
using System.Resources;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("RSSDP2")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("RSSDP")]
[assembly: AssemblyProduct("RSSDP2")]
[assembly: AssemblyCopyright("Copyright © 2016")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: NeutralResourcesLanguage("en")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("c227adb7-e256-4e70-a8b9-22b9e0cf4f55")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@@ -1,23 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<MinimumVisualStudioVersion>12.0</MinimumVisualStudioVersion>
<MinimumVisualStudioVersion>11.0</MinimumVisualStudioVersion>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{67F9D3A8-F71E-4428-913F-C37AE82CDB24}</ProjectGuid>
<ProjectGuid>{21002819-C39A-4D3E-BE83-2A276A77FB1F}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Rssdp</RootNamespace>
<AssemblyName>Rssdp.Portable</AssemblyName>
<ProjectTypeGuids>{786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<TargetFrameworkProfile>Profile44</TargetFrameworkProfile>
<TargetFrameworkVersion>v4.6</TargetFrameworkVersion>
<RootNamespace>RSSDP</RootNamespace>
<AssemblyName>RSSDP</AssemblyName>
<DefaultLanguage>en-US</DefaultLanguage>
<FileAlignment>512</FileAlignment>
<NuGetPackageImportStamp>1c5b2aa5</NuGetPackageImportStamp>
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir>
<RestorePackages>true</RestorePackages>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<ProjectTypeGuids>{786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<TargetFrameworkProfile>Profile7</TargetFrameworkProfile>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
@@ -27,79 +24,57 @@
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<RunCodeAnalysis>true</RunCodeAnalysis>
<CodeAnalysisRuleSet>..\RssdpRuleset.ruleset</CodeAnalysisRuleSet>
<DocumentationFile>bin\Debug\Rssdp.Portable.XML</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>..\lib\portable-net45+win+wpa81+wp80\</OutputPath>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<CodeAnalysisRuleSet>..\RssdpRuleset.ruleset</CodeAnalysisRuleSet>
<DocumentationFile>..\lib\portable-net45+win+wpa81+wp80\Rssdp.Portable.XML</DocumentationFile>
<RunCodeAnalysis>true</RunCodeAnalysis>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Shared\AssemblyInfoCommon.cs">
<Link>Properties\AssemblyInfoCommon.cs</Link>
</Compile>
<Compile Include="CustomHttpHeaders.cs" />
<Compile Include="DeviceAvailableEventArgs.cs" />
<Compile Include="DeviceEventArgs.cs" />
<Compile Include="DeviceUnavailableEventArgs.cs" />
<Compile Include="DiscoveredSsdpDevice.cs" />
<Compile Include="DisposableManagedObjectBase.cs" />
<Compile Include="GlobalSuppressions.cs" />
<Compile Include="HttpParserBase.cs" />
<Compile Include="HttpRequestParser.cs" />
<Compile Include="HttpResponseParser.cs" />
<Compile Include="IEnumerableExtensions.cs" />
<Compile Include="ISsdpCommunicationsServer.cs" />
<Compile Include="ISsdpDeviceLocator.cs" />
<Compile Include="ISsdpDevicePublisher.cs" />
<Compile Include="IUPnPDeviceValidator.cs" />
<Compile Include="ReadOnlyEnumerable.cs" />
<Compile Include="SsdpDeviceExtensions.cs" />
<Compile Include="SsdpDeviceLocatorBase.cs" />
<Compile Include="DiscoveredSsdpDevice.cs" />
<Compile Include="GlobalSuppressions.cs" />
<Compile Include="HttpRequestParser.cs" />
<Compile Include="HttpResponseParser.cs" />
<Compile Include="ISocketFactory.cs" />
<Compile Include="IUdpSocket.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ReceivedUdpData.cs" />
<Compile Include="ReadOnlyEnumerable.cs" />
<Compile Include="RequestReceivedEventArgs.cs" />
<Compile Include="ResponseReceivedEventArgs.cs" />
<Compile Include="SsdpCommunicationsServer.cs" />
<Compile Include="SsdpConstants.cs" />
<Compile Include="SsdpDevice.cs" />
<Compile Include="SsdpDeviceExtensions.cs" />
<Compile Include="SsdpDeviceIcon.cs" />
<Compile Include="SsdpDeviceLocator.cs" />
<Compile Include="SsdpDeviceLocatorBase.cs" />
<Compile Include="SsdpDeviceProperties.cs" />
<Compile Include="SsdpDeviceProperty.cs" />
<Compile Include="SsdpDevicePublisher.cs" />
<Compile Include="SsdpDevicePublisherBase.cs" />
<Compile Include="SsdpEmbeddedDevice.cs" />
<Compile Include="SsdpRootDevice.cs" />
<Compile Include="UdpEndPoint.cs" />
<Compile Include="UPnP10DeviceValidator.cs" />
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<CodeAnalysisDictionary Include="..\Shared\CodeAnalysisDictionary.xml">
<Link>Properties\CodeAnalysisDictionary.xml</Link>
<SubType>Designer</SubType>
</CodeAnalysisDictionary>
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
<Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
<Name>MediaBrowser.Model</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\packages\Microsoft.Bcl.Build.1.0.21\build\Microsoft.Bcl.Build.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Bcl.Build.1.0.21\build\Microsoft.Bcl.Build.targets'))" />
</Target>
<Import Project="..\packages\Microsoft.Bcl.Build.1.0.21\build\Microsoft.Bcl.Build.targets" Condition="Exists('..\packages\Microsoft.Bcl.Build.1.0.21\build\Microsoft.Bcl.Build.targets')" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>c227adb7-e256-4e70-a8b9-22b9e0cf4f55</ProjectGuid>
<RootNamespace>RSSDP</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@@ -1,29 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Rssdp.Infrastructure
{
/// <summary>
/// Used by the sockets wrapper to hold raw data received from a UDP socket.
/// </summary>
public sealed class ReceivedUdpData
{
/// <summary>
/// The buffer to place received data into.
/// </summary>
public byte[] Buffer { get; set; }
/// <summary>
/// The number of bytes received.
/// </summary>
public int ReceivedBytes { get; set; }
/// <summary>
/// The <see cref="UdpEndPoint"/> the data was received from.
/// </summary>
public UdpEndPoint ReceivedFrom { get; set; }
}
}

View File

@@ -5,6 +5,7 @@ using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using MediaBrowser.Model.Net;
namespace Rssdp.Infrastructure
{
@@ -17,7 +18,7 @@ namespace Rssdp.Infrastructure
#region Fields
private readonly HttpRequestMessage _Message;
private readonly UdpEndPoint _ReceivedFrom;
private readonly IpEndPointInfo _ReceivedFrom;
#endregion
@@ -28,7 +29,7 @@ namespace Rssdp.Infrastructure
/// </summary>
/// <param name="message">The <see cref="HttpRequestMessage"/> that was received.</param>
/// <param name="receivedFrom">A <see cref="UdpEndPoint"/> representing the sender's address (sometimes used for replies).</param>
public RequestReceivedEventArgs(HttpRequestMessage message, UdpEndPoint receivedFrom)
public RequestReceivedEventArgs(HttpRequestMessage message, IpEndPointInfo receivedFrom)
{
_Message = message;
_ReceivedFrom = receivedFrom;
@@ -49,7 +50,7 @@ namespace Rssdp.Infrastructure
/// <summary>
/// The <see cref="UdpEndPoint"/> the request came from.
/// </summary>
public UdpEndPoint ReceivedFrom
public IpEndPointInfo ReceivedFrom
{
get { return _ReceivedFrom; }
}

View File

@@ -5,6 +5,7 @@ using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using MediaBrowser.Model.Net;
namespace Rssdp.Infrastructure
{
@@ -17,7 +18,7 @@ namespace Rssdp.Infrastructure
#region Fields
private readonly HttpResponseMessage _Message;
private readonly UdpEndPoint _ReceivedFrom;
private readonly IpEndPointInfo _ReceivedFrom;
#endregion
@@ -26,9 +27,7 @@ namespace Rssdp.Infrastructure
/// <summary>
/// Full constructor.
/// </summary>
/// <param name="message">The <see cref="HttpResponseMessage"/> that was received.</param>
/// <param name="receivedFrom">A <see cref="UdpEndPoint"/> representing the sender's address (sometimes used for replies).</param>
public ResponseReceivedEventArgs(HttpResponseMessage message, UdpEndPoint receivedFrom)
public ResponseReceivedEventArgs(HttpResponseMessage message, IpEndPointInfo receivedFrom)
{
_Message = message;
_ReceivedFrom = receivedFrom;
@@ -49,7 +48,7 @@ namespace Rssdp.Infrastructure
/// <summary>
/// The <see cref="UdpEndPoint"/> the response came from.
/// </summary>
public UdpEndPoint ReceivedFrom
public IpEndPointInfo ReceivedFrom
{
get { return _ReceivedFrom; }
}

View File

@@ -1,114 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Security;
using System.Text;
using Rssdp.Infrastructure;
namespace Rssdp
{
// THIS IS A LINKED FILE - SHARED AMONGST MULTIPLE PLATFORMS
// Be careful to check any changes compile and work for all platform projects it is shared in.
// Not entirely happy with this. Would have liked to have done something more generic/reusable,
// but that wasn't really the point so kept to YAGNI principal for now, even if the
// interfaces are a bit ugly, specific and make assumptions.
/// <summary>
/// Used by RSSDP components to create implementations of the <see cref="IUdpSocket"/> interface, to perform platform agnostic socket communications.
/// </summary>
public sealed class SocketFactory : ISocketFactory
{
private IPAddress _LocalIP;
/// <summary>
/// Default constructor.
/// </summary>
/// <param name="localIP">A string containing the IP address of the local network adapter to bind sockets to. Null or empty string will use <see cref="IPAddress.Any"/>.</param>
public SocketFactory(string localIP)
{
if (String.IsNullOrEmpty(localIP))
_LocalIP = IPAddress.Any;
else
_LocalIP = IPAddress.Parse(localIP);
}
#region ISocketFactory Members
/// <summary>
/// Creates a new UDP socket that is a member of the SSDP multicast local admin group and binds it to the specified local port.
/// </summary>
/// <param name="localPort">An integer specifying the local port to bind the socket to.</param>
/// <returns>An implementation of the <see cref="IUdpSocket"/> interface used by RSSDP components to perform socket operations.</returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "The purpose of this method is to create and returns a disposable result, it is up to the caller to dispose it when they are done with it.")]
public IUdpSocket CreateUdpSocket(int localPort)
{
if (localPort < 0) throw new ArgumentException("localPort cannot be less than zero.", "localPort");
var retVal = new Socket(System.Net.Sockets.AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Dgram, System.Net.Sockets.ProtocolType.Udp);
try
{
retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, SsdpConstants.SsdpDefaultMulticastTimeToLive);
retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(IPAddress.Parse(SsdpConstants.MulticastLocalAdminAddress), _LocalIP));
return new UdpSocket(retVal, localPort, _LocalIP.ToString());
}
catch
{
if (retVal != null)
retVal.Dispose();
throw;
}
}
/// <summary>
/// Creates a new UDP socket that is a member of the specified multicast IP address, and binds it to the specified local port.
/// </summary>
/// <param name="ipAddress">The multicast IP address to make the socket a member of.</param>
/// <param name="multicastTimeToLive">The multicast time to live value for the socket.</param>
/// <param name="localPort">The number of the local port to bind to.</param>
/// <returns></returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "ip"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "The purpose of this method is to create and returns a disposable result, it is up to the caller to dispose it when they are done with it.")]
public IUdpSocket CreateUdpMulticastSocket(string ipAddress, int multicastTimeToLive, int localPort)
{
if (ipAddress == null) throw new ArgumentNullException("ipAddress");
if (ipAddress.Length == 0) throw new ArgumentException("ipAddress cannot be an empty string.", "ipAddress");
if (multicastTimeToLive <= 0) throw new ArgumentException("multicastTimeToLive cannot be zero or less.", "multicastTimeToLive");
if (localPort < 0) throw new ArgumentException("localPort cannot be less than zero.", "localPort");
var retVal = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
try
{
#if NETSTANDARD1_3
// The ExclusiveAddressUse socket option is a Windows-specific option that, when set to "true," tells Windows not to allow another socket to use the same local address as this socket
// See https://github.com/dotnet/corefx/pull/11509 for more details
if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows))
{
retVal.ExclusiveAddressUse = false;
}
#else
retVal.ExclusiveAddressUse = false;
#endif
retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive);
retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(IPAddress.Parse(ipAddress), _LocalIP));
retVal.MulticastLoopback = true;
return new UdpSocket(retVal, localPort, _LocalIP.ToString());
}
catch
{
if (retVal != null)
retVal.Dispose();
throw;
}
}
#endregion
}
}

View File

@@ -5,6 +5,7 @@ using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using MediaBrowser.Model.Net;
namespace Rssdp.Infrastructure
{
@@ -157,10 +158,10 @@ namespace Rssdp.Infrastructure
/// Sends a message to a particular address (uni or multicast) and port.
/// </summary>
/// <param name="messageData">A byte array containing the data to send.</param>
/// <param name="destination">A <see cref="UdpEndPoint"/> representing the destination address for the data. Can be either a multicast or unicast destination.</param>
/// <param name="destination">A <see cref="IpEndPointInfo"/> representing the destination address for the data. Can be either a multicast or unicast destination.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the <paramref name="messageData"/> argument is null.</exception>
/// <exception cref="System.ObjectDisposedException">Thrown if the <see cref="DisposableManagedObjectBase.IsDisposed"/> property is true (because <seealso cref="DisposableManagedObjectBase.Dispose()" /> has been called previously).</exception>
public async Task SendMessage(byte[] messageData, UdpEndPoint destination)
public async Task SendMessage(byte[] messageData, IpEndPointInfo destination)
{
if (messageData == null) throw new ArgumentNullException("messageData");
@@ -188,7 +189,7 @@ namespace Rssdp.Infrastructure
// SSDP spec recommends sending messages multiple times (not more than 3) to account for possible packet loss over UDP.
await Repeat(SsdpConstants.UdpResendCount, TimeSpan.FromMilliseconds(100),
() => SendMessageIfSocketNotDisposed(messageData, new UdpEndPoint() { IPAddress = SsdpConstants.MulticastLocalAdminAddress, Port = SsdpConstants.MulticastPort })).ConfigureAwait(false);
() => SendMessageIfSocketNotDisposed(messageData, new IpEndPointInfo() { IpAddress = new IpAddressInfo { Address = SsdpConstants.MulticastLocalAdminAddress }, Port = SsdpConstants.MulticastPort })).ConfigureAwait(false);
}
/// <summary>
@@ -254,7 +255,7 @@ namespace Rssdp.Infrastructure
#region Private Methods
private async Task SendMessageIfSocketNotDisposed(byte[] messageData, UdpEndPoint destination)
private async Task SendMessageIfSocketNotDisposed(byte[] messageData, IpEndPointInfo destination)
{
var socket = _SendSocket;
if (socket != null)
@@ -343,7 +344,7 @@ namespace Rssdp.Infrastructure
}
}
private void ProcessMessage(string data, UdpEndPoint endPoint)
private void ProcessMessage(string data, IpEndPointInfo endPoint)
{
//Responses start with the HTTP version, prefixed with HTTP/ while
//requests start with a method which can vary and might be one we haven't
@@ -375,7 +376,7 @@ namespace Rssdp.Infrastructure
}
}
private void OnRequestReceived(HttpRequestMessage data, UdpEndPoint endPoint)
private void OnRequestReceived(HttpRequestMessage data, IpEndPointInfo endPoint)
{
//SSDP specification says only * is currently used but other uri's might
//be implemented in the future and should be ignored unless understood.
@@ -387,7 +388,7 @@ namespace Rssdp.Infrastructure
handlers(this, new RequestReceivedEventArgs(data, endPoint));
}
private void OnResponseReceived(HttpResponseMessage data, UdpEndPoint endPoint)
private void OnResponseReceived(HttpResponseMessage data, IpEndPointInfo endPoint)
{
var handlers = this.ResponseReceived;
if (handlers != null)

View File

@@ -2,6 +2,8 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Threading;
using Rssdp.Infrastructure;
namespace Rssdp
@@ -19,7 +21,7 @@ namespace Rssdp
/// Default constructor. Constructs a new instance using the default <see cref="ISsdpCommunicationsServer"/> and <see cref="ISocketFactory"/> implementations for this platform.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification="Can't expose along exception paths here (exceptions should be very rare anyway, and probably fatal too) and we shouldn't dipose the items we pass to base in any other case.")]
public SsdpDeviceLocator() : base(new SsdpCommunicationsServer(new SocketFactory(null)))
public SsdpDeviceLocator(ISocketFactory socketFactory, ITimerFactory timerFacatory) : base(new SsdpCommunicationsServer(socketFactory), timerFacatory)
{
// This is not the problem you are looking for;
// Yes, this is poor man's dependency injection which some call an anti-pattern.
@@ -30,14 +32,5 @@ namespace Rssdp
// There is a constructor that takes a manually injected dependency anyway, so proper DI using
// a container or whatever can be done anyway.
}
/// <summary>
/// Full constructor. Constructs a new instance using the provided <see cref="ISsdpCommunicationsServer"/> implementation.
/// </summary>
public SsdpDeviceLocator(ISsdpCommunicationsServer communicationsServer)
: base(communicationsServer)
{
}
}
}

View File

@@ -7,6 +7,7 @@ using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Threading;
namespace Rssdp.Infrastructure
{
@@ -24,7 +25,8 @@ namespace Rssdp.Infrastructure
private IList<DiscoveredSsdpDevice> _SearchResults;
private object _SearchResultsSynchroniser;
private System.Threading.Timer _ExpireCachedDevicesTimer;
private ITimer _ExpireCachedDevicesTimer;
private ITimerFactory _timerFactory;
private const string HttpURequestMessageFormat = @"{0} * HTTP/1.1
HOST: {1}:{2}
@@ -44,12 +46,12 @@ ST: {4}
/// <summary>
/// Default constructor.
/// </summary>
/// <param name="communicationsServer">The <see cref="ISsdpCommunicationsServer"/> implementation to use for network communications.</param>
protected SsdpDeviceLocatorBase(ISsdpCommunicationsServer communicationsServer)
protected SsdpDeviceLocatorBase(ISsdpCommunicationsServer communicationsServer, ITimerFactory timerFactory)
{
if (communicationsServer == null) throw new ArgumentNullException("communicationsServer");
_CommunicationsServer = communicationsServer;
_timerFactory = timerFactory;
_CommunicationsServer.ResponseReceived += CommsServer_ResponseReceived;
_SearchResultsSynchroniser = new object();
@@ -521,7 +523,7 @@ ST: {4}
if (IsDisposed) return;
if (_ExpireCachedDevicesTimer == null)
_ExpireCachedDevicesTimer = new Timer(this.ExpireCachedDevices, null, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
_ExpireCachedDevicesTimer = _timerFactory.Create(this.ExpireCachedDevices, null, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
_ExpireCachedDevicesTimer.Change(60000, System.Threading.Timeout.Infinite);
}

View File

@@ -2,6 +2,8 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Threading;
using Rssdp.Infrastructure;
namespace Rssdp
@@ -24,79 +26,13 @@ namespace Rssdp
/// <para>Uses the default <see cref="ISsdpCommunicationsServer"/> implementation and network settings for Windows and the SSDP specification.</para>
/// </remarks>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "No way to do this here, and we don't want to dispose it except in the (rare) case of an exception anyway.")]
public SsdpDevicePublisher()
: this(new SsdpCommunicationsServer(new SocketFactory(null)))
public SsdpDevicePublisher(ISocketFactory socketFactory, ITimerFactory timerFactory, string osName, string osVersion)
: base(new SsdpCommunicationsServer(socketFactory), timerFactory, osName, osVersion)
{
}
/// <summary>
/// Full constructor.
/// </summary>
/// <remarks>
/// <para>Allows the caller to specify their own <see cref="ISsdpCommunicationsServer"/> implementation for full control over the networking, or for mocking/testing purposes..</para>
/// </remarks>
public SsdpDevicePublisher(ISsdpCommunicationsServer communicationsServer)
: base(communicationsServer, GetOSName(), GetOSVersion())
{
}
/// <summary>
/// Partial constructor.
/// </summary>
/// <param name="localPort">The local port to use for socket communications, specify 0 to have the system choose it's own.</param>
/// <remarks>
/// <para>Uses the default <see cref="ISsdpCommunicationsServer"/> implementation and network settings for Windows and the SSDP specification, but specifies the local port to use for socket communications. Specify 0 to indicate the system should choose it's own port.</para>
/// </remarks>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "No way to do this here, and we don't want to dispose it except in the (rare) case of an exception anyway.")]
public SsdpDevicePublisher(int localPort)
: this(new SsdpCommunicationsServer(new SocketFactory(null), localPort))
{
}
/// <summary>
/// Partial constructor.
/// </summary>
/// <param name="localPort">The local port to use for socket communications, specify 0 to have the system choose it's own.</param>
/// <param name="multicastTimeToLive">The number of hops a multicast packet can make before it expires. Must be 1 or greater.</param>
/// <remarks>
/// <para>Uses the default <see cref="ISsdpCommunicationsServer"/> implementation and network settings for Windows and the SSDP specification, but specifies the local port to use and multicast time to live setting for socket communications.</para>
/// <para>Specify 0 for the <paramref name="localPort"/> argument to indicate the system should choose it's own port.</para>
/// <para>The <paramref name="multicastTimeToLive"/> is actually a number of 'hops' on the network and not a time based argument.</para>
/// </remarks>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "No way to do this here, and we don't want to dispose it except in the (rare) case of an exception anyway.")]
public SsdpDevicePublisher(int localPort, int multicastTimeToLive)
: this(new SsdpCommunicationsServer(new SocketFactory(null), localPort, multicastTimeToLive))
{
}
#endregion
#region Private Methods
private static string GetOSName()
{
#if NET46
return Environment.OSVersion.Platform.ToString();
#elif NETSTANDARD1_6
return System.Runtime.InteropServices.RuntimeInformation.OSDescription;
#endif
return "Operating System";
}
private static string GetOSVersion()
{
#if NET46
return Environment.OSVersion.Version.ToString() + " " + Environment.OSVersion.ServicePack.ToString();
#elif NETSTANDARD1_6
return System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription;
#endif
return "1.0";
}
#endregion
}
}

View File

@@ -4,6 +4,8 @@ using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Threading;
namespace Rssdp.Infrastructure
{
@@ -24,7 +26,8 @@ namespace Rssdp.Infrastructure
private IList<SsdpRootDevice> _Devices;
private ReadOnlyEnumerable<SsdpRootDevice> _ReadOnlyDevices;
private System.Threading.Timer _RebroadcastAliveNotificationsTimer;
private ITimer _RebroadcastAliveNotificationsTimer;
private ITimerFactory _timerFactory;
//private TimeSpan _RebroadcastAliveNotificationsTimeSpan;
private DateTime _LastNotificationTime;
@@ -47,10 +50,7 @@ namespace Rssdp.Infrastructure
/// <summary>
/// Default constructor.
/// </summary>
/// <param name="communicationsServer">The <see cref="ISsdpCommunicationsServer"/> implementation, used to send and receive SSDP network messages.</param>
/// <param name="osName">Then name of the operating system running the server.</param>
/// <param name="osVersion">The version of the operating system running the server.</param>
protected SsdpDevicePublisherBase(ISsdpCommunicationsServer communicationsServer, string osName, string osVersion)
protected SsdpDevicePublisherBase(ISsdpCommunicationsServer communicationsServer, ITimerFactory timerFactory, string osName, string osVersion)
{
if (communicationsServer == null) throw new ArgumentNullException("communicationsServer");
if (osName == null) throw new ArgumentNullException("osName");
@@ -59,6 +59,7 @@ namespace Rssdp.Infrastructure
if (osVersion.Length == 0) throw new ArgumentException("osVersion cannot be an empty string.", "osName");
_SupportPnpRootDevice = true;
_timerFactory = timerFactory;
_Devices = new List<SsdpRootDevice>();
_ReadOnlyDevices = new ReadOnlyEnumerable<SsdpRootDevice>(_Devices);
_RecentSearchRequests = new Dictionary<string, SearchRequest>(StringComparer.OrdinalIgnoreCase);
@@ -234,7 +235,7 @@ namespace Rssdp.Infrastructure
#region Search Related Methods
private void ProcessSearchRequest(string mx, string searchTarget, UdpEndPoint endPoint)
private void ProcessSearchRequest(string mx, string searchTarget, IpEndPointInfo endPoint)
{
if (String.IsNullOrEmpty(searchTarget))
{
@@ -305,7 +306,7 @@ namespace Rssdp.Infrastructure
return _Devices.Union(_Devices.SelectManyRecursive<SsdpDevice>((d) => d.Devices));
}
private void SendDeviceSearchResponses(SsdpDevice device, UdpEndPoint endPoint)
private void SendDeviceSearchResponses(SsdpDevice device, IpEndPointInfo endPoint)
{
bool isRootDevice = (device as SsdpRootDevice) != null;
if (isRootDevice)
@@ -325,7 +326,7 @@ namespace Rssdp.Infrastructure
return String.Format("{0}::{1}", udn, fullDeviceType);
}
private async void SendSearchResponse(string searchTarget, SsdpDevice device, string uniqueServiceName, UdpEndPoint endPoint)
private async void SendSearchResponse(string searchTarget, SsdpDevice device, string uniqueServiceName, IpEndPointInfo endPoint)
{
var rootDevice = device.ToRootDevice();
@@ -357,7 +358,7 @@ namespace Rssdp.Infrastructure
WriteTrace(String.Format("Sent search response to " + endPoint.ToString()), device);
}
private bool IsDuplicateSearchRequest(string searchTarget, UdpEndPoint endPoint)
private bool IsDuplicateSearchRequest(string searchTarget, IpEndPointInfo endPoint)
{
var isDuplicateRequest = false;
@@ -590,7 +591,7 @@ namespace Rssdp.Infrastructure
}
//_RebroadcastAliveNotificationsTimeSpan = rebroadCastInterval;
_RebroadcastAliveNotificationsTimer = new System.Threading.Timer(SendAllAliveNotifications, null, nextBroadcastInterval, rebroadCastInterval);
_RebroadcastAliveNotificationsTimer = _timerFactory.Create(SendAllAliveNotifications, null, nextBroadcastInterval, rebroadCastInterval);
WriteTrace(String.Format("Rebroadcast Interval = {0}, Next Broadcast At = {1}", rebroadCastInterval.ToString(), nextBroadcastInterval.ToString()));
}
@@ -704,7 +705,7 @@ namespace Rssdp.Infrastructure
private class SearchRequest
{
public UdpEndPoint EndPoint { get; set; }
public IpEndPointInfo EndPoint { get; set; }
public DateTime Received { get; set; }
public string SearchTarget { get; set; }

View File

@@ -1,37 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Rssdp.Infrastructure
{
/// <summary>
/// Cross platform representation of a UDP end point, being an IP address (either IPv4 or IPv6) and a port.
/// </summary>
public sealed class UdpEndPoint
{
/// <summary>
/// The IP Address of the end point.
/// </summary>
/// <remarks>
/// <para>Can be either IPv4 or IPv6, up to the code using this instance to determine which was provided.</para>
/// </remarks>
public string IPAddress { get; set; }
/// <summary>
/// The port of the end point.
/// </summary>
public int Port { get; set; }
/// <summary>
/// Returns the <see cref="IPAddress"/> and <see cref="Port"/> values separated by a colon.
/// </summary>
/// <returns>A string containing <see cref="IPAddress"/>:<see cref="Port"/>.</returns>
public override string ToString()
{
return (this.IPAddress ?? String.Empty) + ":" + this.Port.ToString();
}
}
}

View File

@@ -1,259 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Security;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Rssdp.Infrastructure;
namespace Rssdp
{
// THIS IS A LINKED FILE - SHARED AMONGST MULTIPLE PLATFORMS
// Be careful to check any changes compile and work for all platform projects it is shared in.
internal sealed class UdpSocket : DisposableManagedObjectBase, IUdpSocket
{
#region Fields
private System.Net.Sockets.Socket _Socket;
private int _LocalPort;
#endregion
#region Constructors
public UdpSocket(System.Net.Sockets.Socket socket, int localPort, string ipAddress)
{
if (socket == null) throw new ArgumentNullException("socket");
_Socket = socket;
_LocalPort = localPort;
IPAddress ip = null;
if (String.IsNullOrEmpty(ipAddress))
ip = IPAddress.Any;
else
ip = IPAddress.Parse(ipAddress);
_Socket.Bind(new IPEndPoint(ip, _LocalPort));
if (_LocalPort == 0)
_LocalPort = (_Socket.LocalEndPoint as IPEndPoint).Port;
}
#endregion
#region IUdpSocket Members
public System.Threading.Tasks.Task<ReceivedUdpData> ReceiveAsync()
{
ThrowIfDisposed();
var tcs = new TaskCompletionSource<ReceivedUdpData>();
System.Net.EndPoint receivedFromEndPoint = new IPEndPoint(IPAddress.Any, 0);
var state = new AsyncReceiveState(_Socket, receivedFromEndPoint);
state.TaskCompletionSource = tcs;
#if NETSTANDARD1_6
_Socket.ReceiveFromAsync(new System.ArraySegment<Byte>(state.Buffer), System.Net.Sockets.SocketFlags.None, state.EndPoint)
.ContinueWith((task, asyncState) =>
{
if (task.Status != TaskStatus.Faulted)
{
var receiveState = asyncState as AsyncReceiveState;
receiveState.EndPoint = task.Result.RemoteEndPoint;
ProcessResponse(receiveState, () => task.Result.ReceivedBytes);
}
}, state);
#else
_Socket.BeginReceiveFrom(state.Buffer, 0, state.Buffer.Length, System.Net.Sockets.SocketFlags.None, ref state.EndPoint, new AsyncCallback(this.ProcessResponse), state);
#endif
return tcs.Task;
}
public Task SendTo(byte[] messageData, UdpEndPoint endPoint)
{
ThrowIfDisposed();
if (messageData == null) throw new ArgumentNullException("messageData");
if (endPoint == null) throw new ArgumentNullException("endPoint");
#if NETSTANDARD1_6
_Socket.SendTo(messageData, new System.Net.IPEndPoint(IPAddress.Parse(endPoint.IPAddress), endPoint.Port));
return Task.FromResult(true);
#else
var taskSource = new TaskCompletionSource<bool>();
try
{
_Socket.BeginSendTo(messageData, 0, messageData.Length, SocketFlags.None, new System.Net.IPEndPoint(IPAddress.Parse(endPoint.IPAddress), endPoint.Port), result =>
{
try
{
_Socket.EndSend(result);
taskSource.TrySetResult(true);
}
catch (SocketException ex)
{
taskSource.TrySetException(ex);
}
catch (ObjectDisposedException ex)
{
taskSource.TrySetException(ex);
}
catch (InvalidOperationException ex)
{
taskSource.TrySetException(ex);
}
catch (SecurityException ex)
{
taskSource.TrySetException(ex);
}
}, null);
}
catch (SocketException ex)
{
taskSource.TrySetException(ex);
}
catch (ObjectDisposedException ex)
{
taskSource.TrySetException(ex);
}
catch (SecurityException ex)
{
taskSource.TrySetException(ex);
}
//_Socket.SendTo(messageData, new System.Net.IPEndPoint(IPAddress.Parse(endPoint.IPAddress), endPoint.Port));
return taskSource.Task;
#endif
}
#endregion
#region Overrides
protected override void Dispose(bool disposing)
{
if (disposing)
{
var socket = _Socket;
if (socket != null)
socket.Dispose();
}
}
#endregion
#region Private Methods
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions via task methods should be reported by task completion source, so this should be ok.")]
private static void ProcessResponse(AsyncReceiveState state, Func<int> receiveData)
{
try
{
var bytesRead = receiveData();
var ipEndPoint = state.EndPoint as IPEndPoint;
state.TaskCompletionSource.SetResult(
new ReceivedUdpData()
{
Buffer = state.Buffer,
ReceivedBytes = bytesRead,
ReceivedFrom = new UdpEndPoint()
{
IPAddress = ipEndPoint.Address.ToString(),
Port = ipEndPoint.Port
}
}
);
}
catch (ObjectDisposedException)
{
state.TaskCompletionSource.SetCanceled();
}
catch (SocketException se)
{
if (se.SocketErrorCode != SocketError.Interrupted && se.SocketErrorCode != SocketError.OperationAborted && se.SocketErrorCode != SocketError.Shutdown)
state.TaskCompletionSource.SetException(se);
else
state.TaskCompletionSource.SetCanceled();
}
catch (Exception ex)
{
state.TaskCompletionSource.SetException(ex);
}
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exceptions via task methods should be reported by task completion source, so this should be ok.")]
private void ProcessResponse(IAsyncResult asyncResult)
{
#if NET46
var state = asyncResult.AsyncState as AsyncReceiveState;
try
{
var bytesRead = state.Socket.EndReceiveFrom(asyncResult, ref state.EndPoint);
var ipEndPoint = state.EndPoint as IPEndPoint;
state.TaskCompletionSource.SetResult(
new ReceivedUdpData()
{
Buffer = state.Buffer,
ReceivedBytes = bytesRead,
ReceivedFrom = new UdpEndPoint()
{
IPAddress = ipEndPoint.Address.ToString(),
Port = ipEndPoint.Port
}
}
);
}
catch (ObjectDisposedException)
{
state.TaskCompletionSource.SetCanceled();
}
catch (SocketException se)
{
if (se.SocketErrorCode != SocketError.Interrupted && se.SocketErrorCode != SocketError.OperationAborted && se.SocketErrorCode != SocketError.Shutdown)
state.TaskCompletionSource.SetException(se);
else
state.TaskCompletionSource.SetCanceled();
}
catch (Exception ex)
{
state.TaskCompletionSource.SetException(ex);
}
#endif
}
#endregion
#region Private Classes
private class AsyncReceiveState
{
public AsyncReceiveState(System.Net.Sockets.Socket socket, EndPoint endPoint)
{
this.Socket = socket;
this.EndPoint = endPoint;
}
public EndPoint EndPoint;
public byte[] Buffer = new byte[SsdpConstants.DefaultUdpSocketBufferSize];
public System.Net.Sockets.Socket Socket { get; private set; }
public TaskCompletionSource<ReceivedUdpData> TaskCompletionSource { get; set; }
}
#endregion
}
}

View File

@@ -1,48 +0,0 @@
{
"version": "1.0.0-*",
"dependencies": {
},
"frameworks": {
"net46": {
"frameworkAssemblies": {
"System.Collections": "4.0.0.0",
"System.Net": "4.0.0.0",
"System.Net.Http": "4.0.0.0",
"System.Runtime": "4.0.0.0",
"System.Threading": "4.0.0.0",
"System.Threading.Tasks": "4.0.0.0",
"System.Xml": "4.0.0.0"
},
"dependencies": {
}
},
"netstandard1.6": {
"imports": "dnxcore50",
"dependencies": {
"NETStandard.Library": "1.6.0",
"System.Collections": "4.0.11",
"System.Diagnostics.Debug": "4.0.11",
"System.Diagnostics.Tools": "4.0.1",
"System.IO": "4.1.0",
"System.Linq": "4.1.0",
"System.Net.Http": "4.1.0",
"System.Net.Primitives": "4.0.11",
"System.Net.Sockets": "4.1.0",
"System.Resources.ResourceManager": "4.0.1",
"System.Runtime": "4.1.0",
"System.Runtime.Extensions": "4.1.0",
"System.Runtime.InteropServices.RuntimeInformation": "4.0.0",
"System.Text.Encoding": "4.0.11",
"System.Text.Encoding.Extensions": "4.0.11",
"System.Threading": "4.0.11",
"System.Threading.Tasks": "4.0.11",
"System.Threading.Timer": "4.0.1",
"System.Xml.ReaderWriter": "4.0.11"
}
}
}
}