aboutsummaryrefslogtreecommitdiff
path: root/Emby.Common.Implementations
diff options
context:
space:
mode:
authorLuke <luke.pulverenti@gmail.com>2016-12-18 00:44:33 -0500
committerGitHub <noreply@github.com>2016-12-18 00:44:33 -0500
commite7cebb91a73354dc3e0d0b6340c9fbd6511f4406 (patch)
tree6f1c368c766c17b7514fe749c0e92e69cd89194a /Emby.Common.Implementations
parent025905a3e4d50b9a2e07fbf4ff0a203af6604ced (diff)
parentaaa027f3229073e9a40756c3157d41af2a442922 (diff)
Merge pull request #2350 from MediaBrowser/beta
Beta
Diffstat (limited to 'Emby.Common.Implementations')
-rw-r--r--Emby.Common.Implementations/Archiving/ZipClient.cs194
-rw-r--r--Emby.Common.Implementations/BaseApplicationHost.cs899
-rw-r--r--Emby.Common.Implementations/BaseApplicationPaths.cs174
-rw-r--r--Emby.Common.Implementations/Configuration/BaseConfigurationManager.cs329
-rw-r--r--Emby.Common.Implementations/Configuration/ConfigurationHelper.cs60
-rw-r--r--Emby.Common.Implementations/Cryptography/CryptographyProvider.cs40
-rw-r--r--Emby.Common.Implementations/Devices/DeviceId.cs109
-rw-r--r--Emby.Common.Implementations/Diagnostics/CommonProcess.cs108
-rw-r--r--Emby.Common.Implementations/Diagnostics/ProcessFactory.cs12
-rw-r--r--Emby.Common.Implementations/Emby.Common.Implementations.xproj23
-rw-r--r--Emby.Common.Implementations/EnvironmentInfo/EnvironmentInfo.cs119
-rw-r--r--Emby.Common.Implementations/HttpClientManager/HttpClientInfo.cs16
-rw-r--r--Emby.Common.Implementations/HttpClientManager/HttpClientManager.cs988
-rw-r--r--Emby.Common.Implementations/IO/IsoManager.cs75
-rw-r--r--Emby.Common.Implementations/IO/ManagedFileSystem.cs794
-rw-r--r--Emby.Common.Implementations/Logging/NLogger.cs224
-rw-r--r--Emby.Common.Implementations/Logging/NlogManager.cs544
-rw-r--r--Emby.Common.Implementations/Net/DisposableManagedObjectBase.cs74
-rw-r--r--Emby.Common.Implementations/Net/NetSocket.cs97
-rw-r--r--Emby.Common.Implementations/Net/SocketAcceptor.cs127
-rw-r--r--Emby.Common.Implementations/Net/SocketFactory.cs160
-rw-r--r--Emby.Common.Implementations/Net/UdpSocket.cs242
-rw-r--r--Emby.Common.Implementations/Networking/NetworkManager.cs525
-rw-r--r--Emby.Common.Implementations/Properties/AssemblyInfo.cs19
-rw-r--r--Emby.Common.Implementations/Reflection/AssemblyInfo.cs31
-rw-r--r--Emby.Common.Implementations/ScheduledTasks/DailyTrigger.cs91
-rw-r--r--Emby.Common.Implementations/ScheduledTasks/IntervalTrigger.cs112
-rw-r--r--Emby.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs783
-rw-r--r--Emby.Common.Implementations/ScheduledTasks/StartupTrigger.cs67
-rw-r--r--Emby.Common.Implementations/ScheduledTasks/SystemEventTrigger.cs86
-rw-r--r--Emby.Common.Implementations/ScheduledTasks/TaskManager.cs334
-rw-r--r--Emby.Common.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs215
-rw-r--r--Emby.Common.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs138
-rw-r--r--Emby.Common.Implementations/ScheduledTasks/Tasks/ReloadLoggerFileTask.cs112
-rw-r--r--Emby.Common.Implementations/ScheduledTasks/WeeklyTrigger.cs116
-rw-r--r--Emby.Common.Implementations/Serialization/JsonSerializer.cs227
-rw-r--r--Emby.Common.Implementations/Serialization/XmlSerializer.cs138
-rw-r--r--Emby.Common.Implementations/TextEncoding/TextEncoding.cs43
-rw-r--r--Emby.Common.Implementations/Threading/CommonTimer.cs39
-rw-r--r--Emby.Common.Implementations/Threading/TimerFactory.cs21
-rw-r--r--Emby.Common.Implementations/Xml/XmlReaderSettingsFactory.cs22
-rw-r--r--Emby.Common.Implementations/project.json71
42 files changed, 8598 insertions, 0 deletions
diff --git a/Emby.Common.Implementations/Archiving/ZipClient.cs b/Emby.Common.Implementations/Archiving/ZipClient.cs
new file mode 100644
index 0000000000..791c6678cd
--- /dev/null
+++ b/Emby.Common.Implementations/Archiving/ZipClient.cs
@@ -0,0 +1,194 @@
+using System.IO;
+using MediaBrowser.Model.IO;
+using SharpCompress.Archives.Rar;
+using SharpCompress.Archives.SevenZip;
+using SharpCompress.Archives.Tar;
+using SharpCompress.Common;
+using SharpCompress.Readers;
+using SharpCompress.Readers.Zip;
+
+namespace Emby.Common.Implementations.Archiving
+{
+ /// <summary>
+ /// Class DotNetZipClient
+ /// </summary>
+ public class ZipClient : IZipClient
+ {
+ private readonly IFileSystem _fileSystem;
+
+ public ZipClient(IFileSystem fileSystem)
+ {
+ _fileSystem = fileSystem;
+ }
+
+ /// <summary>
+ /// Extracts all.
+ /// </summary>
+ /// <param name="sourceFile">The source file.</param>
+ /// <param name="targetPath">The target path.</param>
+ /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
+ public void ExtractAll(string sourceFile, string targetPath, bool overwriteExistingFiles)
+ {
+ using (var fileStream = _fileSystem.OpenRead(sourceFile))
+ {
+ ExtractAll(fileStream, targetPath, overwriteExistingFiles);
+ }
+ }
+
+ /// <summary>
+ /// Extracts all.
+ /// </summary>
+ /// <param name="source">The source.</param>
+ /// <param name="targetPath">The target path.</param>
+ /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
+ public void ExtractAll(Stream source, string targetPath, bool overwriteExistingFiles)
+ {
+ using (var reader = ReaderFactory.Open(source))
+ {
+ var options = new ExtractionOptions();
+ options.ExtractFullPath = true;
+
+ if (overwriteExistingFiles)
+ {
+ options.Overwrite = true;
+ }
+
+ reader.WriteAllToDirectory(targetPath, options);
+ }
+ }
+
+ public void ExtractAllFromZip(Stream source, string targetPath, bool overwriteExistingFiles)
+ {
+ using (var reader = ZipReader.Open(source))
+ {
+ var options = new ExtractionOptions();
+ options.ExtractFullPath = true;
+
+ if (overwriteExistingFiles)
+ {
+ options.Overwrite = true;
+ }
+
+ reader.WriteAllToDirectory(targetPath, options);
+ }
+ }
+
+ /// <summary>
+ /// Extracts all from7z.
+ /// </summary>
+ /// <param name="sourceFile">The source file.</param>
+ /// <param name="targetPath">The target path.</param>
+ /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
+ public void ExtractAllFrom7z(string sourceFile, string targetPath, bool overwriteExistingFiles)
+ {
+ using (var fileStream = _fileSystem.OpenRead(sourceFile))
+ {
+ ExtractAllFrom7z(fileStream, targetPath, overwriteExistingFiles);
+ }
+ }
+
+ /// <summary>
+ /// Extracts all from7z.
+ /// </summary>
+ /// <param name="source">The source.</param>
+ /// <param name="targetPath">The target path.</param>
+ /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
+ public void ExtractAllFrom7z(Stream source, string targetPath, bool overwriteExistingFiles)
+ {
+ using (var archive = SevenZipArchive.Open(source))
+ {
+ using (var reader = archive.ExtractAllEntries())
+ {
+ var options = new ExtractionOptions();
+ options.ExtractFullPath = true;
+
+ if (overwriteExistingFiles)
+ {
+ options.Overwrite = true;
+ }
+
+ reader.WriteAllToDirectory(targetPath, options);
+ }
+ }
+ }
+
+
+ /// <summary>
+ /// Extracts all from tar.
+ /// </summary>
+ /// <param name="sourceFile">The source file.</param>
+ /// <param name="targetPath">The target path.</param>
+ /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
+ public void ExtractAllFromTar(string sourceFile, string targetPath, bool overwriteExistingFiles)
+ {
+ using (var fileStream = _fileSystem.OpenRead(sourceFile))
+ {
+ ExtractAllFromTar(fileStream, targetPath, overwriteExistingFiles);
+ }
+ }
+
+ /// <summary>
+ /// Extracts all from tar.
+ /// </summary>
+ /// <param name="source">The source.</param>
+ /// <param name="targetPath">The target path.</param>
+ /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
+ public void ExtractAllFromTar(Stream source, string targetPath, bool overwriteExistingFiles)
+ {
+ using (var archive = TarArchive.Open(source))
+ {
+ using (var reader = archive.ExtractAllEntries())
+ {
+ var options = new ExtractionOptions();
+ options.ExtractFullPath = true;
+
+ if (overwriteExistingFiles)
+ {
+ options.Overwrite = true;
+ }
+
+ reader.WriteAllToDirectory(targetPath, options);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Extracts all from rar.
+ /// </summary>
+ /// <param name="sourceFile">The source file.</param>
+ /// <param name="targetPath">The target path.</param>
+ /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
+ public void ExtractAllFromRar(string sourceFile, string targetPath, bool overwriteExistingFiles)
+ {
+ using (var fileStream = _fileSystem.OpenRead(sourceFile))
+ {
+ ExtractAllFromRar(fileStream, targetPath, overwriteExistingFiles);
+ }
+ }
+
+ /// <summary>
+ /// Extracts all from rar.
+ /// </summary>
+ /// <param name="source">The source.</param>
+ /// <param name="targetPath">The target path.</param>
+ /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
+ public void ExtractAllFromRar(Stream source, string targetPath, bool overwriteExistingFiles)
+ {
+ using (var archive = RarArchive.Open(source))
+ {
+ using (var reader = archive.ExtractAllEntries())
+ {
+ var options = new ExtractionOptions();
+ options.ExtractFullPath = true;
+
+ if (overwriteExistingFiles)
+ {
+ options.Overwrite = true;
+ }
+
+ reader.WriteAllToDirectory(targetPath, options);
+ }
+ }
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/BaseApplicationHost.cs b/Emby.Common.Implementations/BaseApplicationHost.cs
new file mode 100644
index 0000000000..02d7cb31fd
--- /dev/null
+++ b/Emby.Common.Implementations/BaseApplicationHost.cs
@@ -0,0 +1,899 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Events;
+using Emby.Common.Implementations.Devices;
+using Emby.Common.Implementations.IO;
+using Emby.Common.Implementations.ScheduledTasks;
+using Emby.Common.Implementations.Serialization;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Common.Progress;
+using MediaBrowser.Common.Security;
+using MediaBrowser.Common.Updates;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Model.Updates;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Extensions;
+using Emby.Common.Implementations.Cryptography;
+using Emby.Common.Implementations.Diagnostics;
+using Emby.Common.Implementations.Net;
+using Emby.Common.Implementations.EnvironmentInfo;
+using Emby.Common.Implementations.Threading;
+using MediaBrowser.Common;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Model.Cryptography;
+using MediaBrowser.Model.Diagnostics;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.System;
+using MediaBrowser.Model.Tasks;
+using MediaBrowser.Model.Threading;
+
+#if NETSTANDARD1_6
+using System.Runtime.Loader;
+#endif
+
+namespace Emby.Common.Implementations
+{
+ /// <summary>
+ /// Class BaseApplicationHost
+ /// </summary>
+ /// <typeparam name="TApplicationPathsType">The type of the T application paths type.</typeparam>
+ public abstract class BaseApplicationHost<TApplicationPathsType> : IApplicationHost
+ where TApplicationPathsType : class, IApplicationPaths
+ {
+ /// <summary>
+ /// Occurs when [has pending restart changed].
+ /// </summary>
+ public event EventHandler HasPendingRestartChanged;
+
+ /// <summary>
+ /// Occurs when [application updated].
+ /// </summary>
+ public event EventHandler<GenericEventArgs<PackageVersionInfo>> ApplicationUpdated;
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance has changes that require the entire application to restart.
+ /// </summary>
+ /// <value><c>true</c> if this instance has pending application restart; otherwise, <c>false</c>.</value>
+ public bool HasPendingRestart { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the logger.
+ /// </summary>
+ /// <value>The logger.</value>
+ protected ILogger Logger { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the plugins.
+ /// </summary>
+ /// <value>The plugins.</value>
+ public IPlugin[] Plugins { get; protected set; }
+
+ /// <summary>
+ /// Gets or sets the log manager.
+ /// </summary>
+ /// <value>The log manager.</value>
+ public ILogManager LogManager { get; protected set; }
+
+ /// <summary>
+ /// Gets the application paths.
+ /// </summary>
+ /// <value>The application paths.</value>
+ protected TApplicationPathsType ApplicationPaths { get; private set; }
+
+ /// <summary>
+ /// The json serializer
+ /// </summary>
+ public IJsonSerializer JsonSerializer { get; private set; }
+
+ /// <summary>
+ /// The _XML serializer
+ /// </summary>
+ protected readonly IXmlSerializer XmlSerializer;
+
+ /// <summary>
+ /// Gets assemblies that failed to load
+ /// </summary>
+ /// <value>The failed assemblies.</value>
+ public List<string> FailedAssemblies { get; protected set; }
+
+ /// <summary>
+ /// Gets all concrete types.
+ /// </summary>
+ /// <value>All concrete types.</value>
+ public Type[] AllConcreteTypes { get; protected set; }
+
+ /// <summary>
+ /// The disposable parts
+ /// </summary>
+ protected readonly List<IDisposable> DisposableParts = new List<IDisposable>();
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is first run.
+ /// </summary>
+ /// <value><c>true</c> if this instance is first run; otherwise, <c>false</c>.</value>
+ public bool IsFirstRun { get; private set; }
+
+ /// <summary>
+ /// Gets the kernel.
+ /// </summary>
+ /// <value>The kernel.</value>
+ protected ITaskManager TaskManager { get; private set; }
+ /// <summary>
+ /// Gets the HTTP client.
+ /// </summary>
+ /// <value>The HTTP client.</value>
+ public IHttpClient HttpClient { get; private set; }
+ /// <summary>
+ /// Gets the network manager.
+ /// </summary>
+ /// <value>The network manager.</value>
+ protected INetworkManager NetworkManager { get; private set; }
+
+ /// <summary>
+ /// Gets the configuration manager.
+ /// </summary>
+ /// <value>The configuration manager.</value>
+ protected IConfigurationManager ConfigurationManager { get; private set; }
+
+ protected IFileSystem FileSystemManager { get; private set; }
+
+ protected IIsoManager IsoManager { get; private set; }
+
+ protected IProcessFactory ProcessFactory { get; private set; }
+ protected ITimerFactory TimerFactory { get; private set; }
+ protected ISocketFactory SocketFactory { get; private set; }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public abstract string Name { get; }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is running as service.
+ /// </summary>
+ /// <value><c>true</c> if this instance is running as service; otherwise, <c>false</c>.</value>
+ public abstract bool IsRunningAsService { get; }
+
+ protected ICryptoProvider CryptographyProvider = new CryptographyProvider();
+
+ protected IEnvironmentInfo EnvironmentInfo { get; private set; }
+
+ private DeviceId _deviceId;
+ public string SystemId
+ {
+ get
+ {
+ if (_deviceId == null)
+ {
+ _deviceId = new DeviceId(ApplicationPaths, LogManager.GetLogger("SystemId"), FileSystemManager);
+ }
+
+ return _deviceId.Value;
+ }
+ }
+
+ public virtual string OperatingSystemDisplayName
+ {
+ get { return EnvironmentInfo.OperatingSystemName; }
+ }
+
+ /// <summary>
+ /// The container
+ /// </summary>
+ protected readonly SimpleInjector.Container Container = new SimpleInjector.Container();
+
+ protected ISystemEvents SystemEvents { get; private set; }
+ protected IMemoryStreamFactory MemoryStreamFactory { get; private set; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BaseApplicationHost{TApplicationPathsType}"/> class.
+ /// </summary>
+ protected BaseApplicationHost(TApplicationPathsType applicationPaths,
+ ILogManager logManager,
+ IFileSystem fileSystem,
+ IEnvironmentInfo environmentInfo,
+ ISystemEvents systemEvents,
+ IMemoryStreamFactory memoryStreamFactory,
+ INetworkManager networkManager)
+ {
+ NetworkManager = networkManager;
+ EnvironmentInfo = environmentInfo;
+ SystemEvents = systemEvents;
+ MemoryStreamFactory = memoryStreamFactory;
+
+ // hack alert, until common can target .net core
+ BaseExtensions.CryptographyProvider = CryptographyProvider;
+
+ XmlSerializer = new MyXmlSerializer(fileSystem, logManager.GetLogger("XmlSerializer"));
+ FailedAssemblies = new List<string>();
+
+ ApplicationPaths = applicationPaths;
+ LogManager = logManager;
+ FileSystemManager = fileSystem;
+
+ ConfigurationManager = GetConfigurationManager();
+
+ // Initialize this early in case the -v command line option is used
+ Logger = LogManager.GetLogger("App");
+ }
+
+ /// <summary>
+ /// Inits this instance.
+ /// </summary>
+ /// <returns>Task.</returns>
+ public virtual async Task Init(IProgress<double> progress)
+ {
+ progress.Report(1);
+
+ JsonSerializer = CreateJsonSerializer();
+
+ OnLoggerLoaded(true);
+ LogManager.LoggerLoaded += (s, e) => OnLoggerLoaded(false);
+
+ IsFirstRun = !ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted;
+ progress.Report(2);
+
+ LogManager.LogSeverity = ConfigurationManager.CommonConfiguration.EnableDebugLevelLogging
+ ? LogSeverity.Debug
+ : LogSeverity.Info;
+
+ progress.Report(3);
+
+ DiscoverTypes();
+ progress.Report(14);
+
+ SetHttpLimit();
+ progress.Report(15);
+
+ var innerProgress = new ActionableProgress<double>();
+ innerProgress.RegisterAction(p => progress.Report(.8 * p + 15));
+
+ await RegisterResources(innerProgress).ConfigureAwait(false);
+
+ FindParts();
+ progress.Report(95);
+
+ await InstallIsoMounters(CancellationToken.None).ConfigureAwait(false);
+
+ progress.Report(100);
+ }
+
+ protected virtual void OnLoggerLoaded(bool isFirstLoad)
+ {
+ Logger.Info("Application version: {0}", ApplicationVersion);
+
+ if (!isFirstLoad)
+ {
+ LogEnvironmentInfo(Logger, ApplicationPaths, false);
+ }
+
+ // Put the app config in the log for troubleshooting purposes
+ Logger.LogMultiline("Application configuration:", LogSeverity.Info, new StringBuilder(JsonSerializer.SerializeToString(ConfigurationManager.CommonConfiguration)));
+
+ if (Plugins != null)
+ {
+ var pluginBuilder = new StringBuilder();
+
+ foreach (var plugin in Plugins)
+ {
+ pluginBuilder.AppendLine(string.Format("{0} {1}", plugin.Name, plugin.Version));
+ }
+
+ Logger.LogMultiline("Plugins:", LogSeverity.Info, pluginBuilder);
+ }
+ }
+
+ public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths, bool isStartup)
+ {
+ logger.LogMultiline("Emby", LogSeverity.Info, GetBaseExceptionMessage(appPaths));
+ }
+
+ protected static StringBuilder GetBaseExceptionMessage(IApplicationPaths appPaths)
+ {
+ var builder = new StringBuilder();
+
+ builder.AppendLine(string.Format("Command line: {0}", string.Join(" ", Environment.GetCommandLineArgs())));
+
+#if NET46
+ builder.AppendLine(string.Format("Operating system: {0}", Environment.OSVersion));
+ builder.AppendLine(string.Format("64-Bit OS: {0}", Environment.Is64BitOperatingSystem));
+ builder.AppendLine(string.Format("64-Bit Process: {0}", Environment.Is64BitProcess));
+
+ Type type = Type.GetType("Mono.Runtime");
+ if (type != null)
+ {
+ MethodInfo displayName = type.GetMethod("GetDisplayName", BindingFlags.NonPublic | BindingFlags.Static);
+ if (displayName != null)
+ {
+ builder.AppendLine("Mono: " + displayName.Invoke(null, null));
+ }
+ }
+#endif
+
+ builder.AppendLine(string.Format("Processor count: {0}", Environment.ProcessorCount));
+ builder.AppendLine(string.Format("Program data path: {0}", appPaths.ProgramDataPath));
+ builder.AppendLine(string.Format("Application directory: {0}", appPaths.ProgramSystemPath));
+
+ return builder;
+ }
+
+ protected abstract IJsonSerializer CreateJsonSerializer();
+
+ private void SetHttpLimit()
+ {
+ try
+ {
+ // Increase the max http request limit
+#if NET46
+ ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit);
+#endif
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error setting http limit", ex);
+ }
+ }
+
+ /// <summary>
+ /// Installs the iso mounters.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ private async Task InstallIsoMounters(CancellationToken cancellationToken)
+ {
+ var list = new List<IIsoMounter>();
+
+ foreach (var isoMounter in GetExports<IIsoMounter>())
+ {
+ try
+ {
+ if (isoMounter.RequiresInstallation && !isoMounter.IsInstalled)
+ {
+ Logger.Info("Installing {0}", isoMounter.Name);
+
+ await isoMounter.Install(cancellationToken).ConfigureAwait(false);
+ }
+
+ list.Add(isoMounter);
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("{0} failed to load.", ex, isoMounter.Name);
+ }
+ }
+
+ IsoManager.AddParts(list);
+ }
+
+ /// <summary>
+ /// Runs the startup tasks.
+ /// </summary>
+ /// <returns>Task.</returns>
+ public virtual Task RunStartupTasks()
+ {
+ Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
+
+ ConfigureAutorun();
+
+ ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
+
+ return Task.FromResult(true);
+ }
+
+ /// <summary>
+ /// Configures the autorun.
+ /// </summary>
+ private void ConfigureAutorun()
+ {
+ try
+ {
+ ConfigureAutoRunAtStartup(ConfigurationManager.CommonConfiguration.RunAtStartup);
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error configuring autorun", ex);
+ }
+ }
+
+ /// <summary>
+ /// Gets the composable part assemblies.
+ /// </summary>
+ /// <returns>IEnumerable{Assembly}.</returns>
+ protected abstract IEnumerable<Assembly> GetComposablePartAssemblies();
+
+ /// <summary>
+ /// Gets the configuration manager.
+ /// </summary>
+ /// <returns>IConfigurationManager.</returns>
+ protected abstract IConfigurationManager GetConfigurationManager();
+
+ /// <summary>
+ /// Finds the parts.
+ /// </summary>
+ protected virtual void FindParts()
+ {
+ ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
+ Plugins = GetExports<IPlugin>().Select(LoadPlugin).Where(i => i != null).ToArray();
+ }
+
+ private IPlugin LoadPlugin(IPlugin plugin)
+ {
+ try
+ {
+ var assemblyPlugin = plugin as IPluginAssembly;
+
+ if (assemblyPlugin != null)
+ {
+#if NET46
+ var assembly = plugin.GetType().Assembly;
+ var assemblyName = assembly.GetName();
+
+ var attribute = (GuidAttribute)assembly.GetCustomAttributes(typeof(GuidAttribute), true)[0];
+ var assemblyId = new Guid(attribute.Value);
+
+ var assemblyFileName = assemblyName.Name + ".dll";
+ var assemblyFilePath = Path.Combine(ApplicationPaths.PluginsPath, assemblyFileName);
+
+ assemblyPlugin.SetAttributes(assemblyFilePath, assemblyFileName, assemblyName.Version, assemblyId);
+#elif NETSTANDARD1_6
+ var typeInfo = plugin.GetType().GetTypeInfo();
+ var assembly = typeInfo.Assembly;
+ var assemblyName = assembly.GetName();
+
+ var attribute = (GuidAttribute)assembly.GetCustomAttribute(typeof(GuidAttribute));
+ var assemblyId = new Guid(attribute.Value);
+
+ var assemblyFileName = assemblyName.Name + ".dll";
+ var assemblyFilePath = Path.Combine(ApplicationPaths.PluginsPath, assemblyFileName);
+
+ assemblyPlugin.SetAttributes(assemblyFilePath, assemblyFileName, assemblyName.Version, assemblyId);
+#else
+return null;
+#endif
+ }
+
+ var isFirstRun = !File.Exists(plugin.ConfigurationFilePath);
+ plugin.SetStartupInfo(isFirstRun, File.GetLastWriteTimeUtc, s => Directory.CreateDirectory(s));
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error loading plugin {0}", ex, plugin.GetType().FullName);
+ return null;
+ }
+
+ return plugin;
+ }
+
+ /// <summary>
+ /// Discovers the types.
+ /// </summary>
+ protected void DiscoverTypes()
+ {
+ FailedAssemblies.Clear();
+
+ var assemblies = GetComposablePartAssemblies().ToList();
+
+ foreach (var assembly in assemblies)
+ {
+ Logger.Info("Loading {0}", assembly.FullName);
+ }
+
+ AllConcreteTypes = assemblies
+ .SelectMany(GetTypes)
+ .Where(t =>
+ {
+#if NET46
+ return t.IsClass && !t.IsAbstract && !t.IsInterface && !t.IsGenericType;
+#endif
+#if NETSTANDARD1_6
+ var typeInfo = t.GetTypeInfo();
+ return typeInfo.IsClass && !typeInfo.IsAbstract && !typeInfo.IsInterface && !typeInfo.IsGenericType;
+#endif
+ return false;
+ })
+ .ToArray();
+ }
+
+ /// <summary>
+ /// Registers resources that classes will depend on
+ /// </summary>
+ /// <returns>Task.</returns>
+ protected virtual Task RegisterResources(IProgress<double> progress)
+ {
+ RegisterSingleInstance(ConfigurationManager);
+ RegisterSingleInstance<IApplicationHost>(this);
+
+ RegisterSingleInstance<IApplicationPaths>(ApplicationPaths);
+
+ TaskManager = new TaskManager(ApplicationPaths, JsonSerializer, LogManager.GetLogger("TaskManager"), FileSystemManager, SystemEvents);
+
+ RegisterSingleInstance(JsonSerializer);
+ RegisterSingleInstance(XmlSerializer);
+ RegisterSingleInstance(MemoryStreamFactory);
+ RegisterSingleInstance(SystemEvents);
+
+ RegisterSingleInstance(LogManager);
+ RegisterSingleInstance(Logger);
+
+ RegisterSingleInstance(TaskManager);
+ RegisterSingleInstance(EnvironmentInfo);
+
+ RegisterSingleInstance(FileSystemManager);
+
+ HttpClient = new HttpClientManager.HttpClientManager(ApplicationPaths, LogManager.GetLogger("HttpClient"), FileSystemManager, MemoryStreamFactory);
+ RegisterSingleInstance(HttpClient);
+
+ RegisterSingleInstance(NetworkManager);
+
+ IsoManager = new IsoManager();
+ RegisterSingleInstance(IsoManager);
+
+ ProcessFactory = new ProcessFactory();
+ RegisterSingleInstance(ProcessFactory);
+
+ TimerFactory = new TimerFactory();
+ RegisterSingleInstance(TimerFactory);
+
+ SocketFactory = new SocketFactory(LogManager.GetLogger("SocketFactory"));
+ RegisterSingleInstance(SocketFactory);
+
+ RegisterSingleInstance(CryptographyProvider);
+
+ return Task.FromResult(true);
+ }
+
+ /// <summary>
+ /// Gets a list of types within an assembly
+ /// This will handle situations that would normally throw an exception - such as a type within the assembly that depends on some other non-existant reference
+ /// </summary>
+ /// <param name="assembly">The assembly.</param>
+ /// <returns>IEnumerable{Type}.</returns>
+ /// <exception cref="System.ArgumentNullException">assembly</exception>
+ protected IEnumerable<Type> GetTypes(Assembly assembly)
+ {
+ if (assembly == null)
+ {
+ throw new ArgumentNullException("assembly");
+ }
+
+ try
+ {
+ return assembly.GetTypes();
+ }
+ catch (ReflectionTypeLoadException ex)
+ {
+ if (ex.LoaderExceptions != null)
+ {
+ foreach (var loaderException in ex.LoaderExceptions)
+ {
+ Logger.Error("LoaderException: " + loaderException.Message);
+ }
+ }
+
+ // If it fails we can still get a list of the Types it was able to resolve
+ return ex.Types.Where(t => t != null);
+ }
+ }
+
+ /// <summary>
+ /// Creates an instance of type and resolves all constructor dependancies
+ /// </summary>
+ /// <param name="type">The type.</param>
+ /// <returns>System.Object.</returns>
+ public object CreateInstance(Type type)
+ {
+ try
+ {
+ return Container.GetInstance(type);
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error creating {0}", ex, type.FullName);
+
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// Creates the instance safe.
+ /// </summary>
+ /// <param name="type">The type.</param>
+ /// <returns>System.Object.</returns>
+ protected object CreateInstanceSafe(Type type)
+ {
+ try
+ {
+ return Container.GetInstance(type);
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error creating {0}", ex, type.FullName);
+ // Don't blow up in release mode
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Registers the specified obj.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="obj">The obj.</param>
+ /// <param name="manageLifetime">if set to <c>true</c> [manage lifetime].</param>
+ protected void RegisterSingleInstance<T>(T obj, bool manageLifetime = true)
+ where T : class
+ {
+ Container.RegisterSingleton(obj);
+
+ if (manageLifetime)
+ {
+ var disposable = obj as IDisposable;
+
+ if (disposable != null)
+ {
+ DisposableParts.Add(disposable);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Registers the single instance.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="func">The func.</param>
+ protected void RegisterSingleInstance<T>(Func<T> func)
+ where T : class
+ {
+ Container.RegisterSingleton(func);
+ }
+
+ /// <summary>
+ /// Resolves this instance.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <returns>``0.</returns>
+ public T Resolve<T>()
+ {
+ return (T)Container.GetRegistration(typeof(T), true).GetInstance();
+ }
+
+ /// <summary>
+ /// Resolves this instance.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <returns>``0.</returns>
+ public T TryResolve<T>()
+ {
+ var result = Container.GetRegistration(typeof(T), false);
+
+ if (result == null)
+ {
+ return default(T);
+ }
+ return (T)result.GetInstance();
+ }
+
+ /// <summary>
+ /// Loads the assembly.
+ /// </summary>
+ /// <param name="file">The file.</param>
+ /// <returns>Assembly.</returns>
+ protected Assembly LoadAssembly(string file)
+ {
+ try
+ {
+#if NET46
+ return Assembly.Load(File.ReadAllBytes(file));
+#elif NETSTANDARD1_6
+
+ return AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(File.ReadAllBytes(file)));
+#endif
+ return null;
+ }
+ catch (Exception ex)
+ {
+ FailedAssemblies.Add(file);
+ Logger.ErrorException("Error loading assembly {0}", ex, file);
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Gets the export types.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <returns>IEnumerable{Type}.</returns>
+ public IEnumerable<Type> GetExportTypes<T>()
+ {
+ var currentType = typeof(T);
+
+#if NET46
+ return AllConcreteTypes.Where(currentType.IsAssignableFrom);
+#elif NETSTANDARD1_6
+ var currentTypeInfo = currentType.GetTypeInfo();
+
+ return AllConcreteTypes.Where(currentTypeInfo.IsAssignableFrom);
+#endif
+ return new List<Type>();
+ }
+
+ /// <summary>
+ /// Gets the exports.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="manageLiftime">if set to <c>true</c> [manage liftime].</param>
+ /// <returns>IEnumerable{``0}.</returns>
+ public IEnumerable<T> GetExports<T>(bool manageLiftime = true)
+ {
+ var parts = GetExportTypes<T>()
+ .Select(CreateInstanceSafe)
+ .Where(i => i != null)
+ .Cast<T>()
+ .ToList();
+
+ if (manageLiftime)
+ {
+ lock (DisposableParts)
+ {
+ DisposableParts.AddRange(parts.OfType<IDisposable>());
+ }
+ }
+
+ return parts;
+ }
+
+ /// <summary>
+ /// Gets the application version.
+ /// </summary>
+ /// <value>The application version.</value>
+ public abstract Version ApplicationVersion { get; }
+
+ /// <summary>
+ /// Handles the ConfigurationUpdated event of the ConfigurationManager control.
+ /// </summary>
+ /// <param name="sender">The source of the event.</param>
+ /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
+ /// <exception cref="System.NotImplementedException"></exception>
+ protected virtual void OnConfigurationUpdated(object sender, EventArgs e)
+ {
+ ConfigureAutorun();
+ }
+
+ protected abstract void ConfigureAutoRunAtStartup(bool autorun);
+
+ /// <summary>
+ /// Removes the plugin.
+ /// </summary>
+ /// <param name="plugin">The plugin.</param>
+ public void RemovePlugin(IPlugin plugin)
+ {
+ var list = Plugins.ToList();
+ list.Remove(plugin);
+ Plugins = list.ToArray();
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance can self restart.
+ /// </summary>
+ /// <value><c>true</c> if this instance can self restart; otherwise, <c>false</c>.</value>
+ public abstract bool CanSelfRestart { get; }
+
+ /// <summary>
+ /// Notifies that the kernel that a change has been made that requires a restart
+ /// </summary>
+ public void NotifyPendingRestart()
+ {
+ var changed = !HasPendingRestart;
+
+ HasPendingRestart = true;
+
+ if (changed)
+ {
+ EventHelper.QueueEventIfNotNull(HasPendingRestartChanged, this, EventArgs.Empty, Logger);
+ }
+ }
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources.
+ /// </summary>
+ /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool dispose)
+ {
+ if (dispose)
+ {
+ var type = GetType();
+
+ Logger.Info("Disposing " + type.Name);
+
+ var parts = DisposableParts.Distinct().Where(i => i.GetType() != type).ToList();
+ DisposableParts.Clear();
+
+ foreach (var part in parts)
+ {
+ Logger.Info("Disposing " + part.GetType().Name);
+
+ try
+ {
+ part.Dispose();
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error disposing {0}", ex, part.GetType().Name);
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Restarts this instance.
+ /// </summary>
+ public abstract Task Restart();
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance can self update.
+ /// </summary>
+ /// <value><c>true</c> if this instance can self update; otherwise, <c>false</c>.</value>
+ public abstract bool CanSelfUpdate { get; }
+
+ /// <summary>
+ /// Checks for update.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="progress">The progress.</param>
+ /// <returns>Task{CheckForUpdateResult}.</returns>
+ public abstract Task<CheckForUpdateResult> CheckForApplicationUpdate(CancellationToken cancellationToken,
+ IProgress<double> progress);
+
+ /// <summary>
+ /// Updates the application.
+ /// </summary>
+ /// <param name="package">The package that contains the update</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="progress">The progress.</param>
+ /// <returns>Task.</returns>
+ public abstract Task UpdateApplication(PackageVersionInfo package, CancellationToken cancellationToken,
+ IProgress<double> progress);
+
+ /// <summary>
+ /// Shuts down.
+ /// </summary>
+ public abstract Task Shutdown();
+
+ /// <summary>
+ /// Called when [application updated].
+ /// </summary>
+ /// <param name="package">The package.</param>
+ protected void OnApplicationUpdated(PackageVersionInfo package)
+ {
+ Logger.Info("Application has been updated to version {0}", package.versionStr);
+
+ EventHelper.FireEventIfNotNull(ApplicationUpdated, this, new GenericEventArgs<PackageVersionInfo>
+ {
+ Argument = package
+
+ }, Logger);
+
+ NotifyPendingRestart();
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/BaseApplicationPaths.cs b/Emby.Common.Implementations/BaseApplicationPaths.cs
new file mode 100644
index 0000000000..8792778ba6
--- /dev/null
+++ b/Emby.Common.Implementations/BaseApplicationPaths.cs
@@ -0,0 +1,174 @@
+using System.IO;
+using MediaBrowser.Common.Configuration;
+
+namespace Emby.Common.Implementations
+{
+ /// <summary>
+ /// Provides a base class to hold common application paths used by both the Ui and Server.
+ /// This can be subclassed to add application-specific paths.
+ /// </summary>
+ public abstract class BaseApplicationPaths : IApplicationPaths
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BaseApplicationPaths"/> class.
+ /// </summary>
+ protected BaseApplicationPaths(string programDataPath, string appFolderPath)
+ {
+ ProgramDataPath = programDataPath;
+ ProgramSystemPath = appFolderPath;
+ }
+
+ public string ProgramDataPath { get; private set; }
+
+ /// <summary>
+ /// Gets the path to the system folder
+ /// </summary>
+ public string ProgramSystemPath { get; private set; }
+
+ /// <summary>
+ /// The _data directory
+ /// </summary>
+ private string _dataDirectory;
+ /// <summary>
+ /// Gets the folder path to the data directory
+ /// </summary>
+ /// <value>The data directory.</value>
+ public string DataPath
+ {
+ get
+ {
+ if (_dataDirectory == null)
+ {
+ _dataDirectory = Path.Combine(ProgramDataPath, "data");
+
+ Directory.CreateDirectory(_dataDirectory);
+ }
+
+ return _dataDirectory;
+ }
+ }
+
+ /// <summary>
+ /// Gets the image cache path.
+ /// </summary>
+ /// <value>The image cache path.</value>
+ public string ImageCachePath
+ {
+ get
+ {
+ return Path.Combine(CachePath, "images");
+ }
+ }
+
+ /// <summary>
+ /// Gets the path to the plugin directory
+ /// </summary>
+ /// <value>The plugins path.</value>
+ public string PluginsPath
+ {
+ get
+ {
+ return Path.Combine(ProgramDataPath, "plugins");
+ }
+ }
+
+ /// <summary>
+ /// Gets the path to the plugin configurations directory
+ /// </summary>
+ /// <value>The plugin configurations path.</value>
+ public string PluginConfigurationsPath
+ {
+ get
+ {
+ return Path.Combine(PluginsPath, "configurations");
+ }
+ }
+
+ /// <summary>
+ /// Gets the path to where temporary update files will be stored
+ /// </summary>
+ /// <value>The plugin configurations path.</value>
+ public string TempUpdatePath
+ {
+ get
+ {
+ return Path.Combine(ProgramDataPath, "updates");
+ }
+ }
+
+ /// <summary>
+ /// Gets the path to the log directory
+ /// </summary>
+ /// <value>The log directory path.</value>
+ public string LogDirectoryPath
+ {
+ get
+ {
+ return Path.Combine(ProgramDataPath, "logs");
+ }
+ }
+
+ /// <summary>
+ /// Gets the path to the application configuration root directory
+ /// </summary>
+ /// <value>The configuration directory path.</value>
+ public string ConfigurationDirectoryPath
+ {
+ get
+ {
+ return Path.Combine(ProgramDataPath, "config");
+ }
+ }
+
+ /// <summary>
+ /// Gets the path to the system configuration file
+ /// </summary>
+ /// <value>The system configuration file path.</value>
+ public string SystemConfigurationFilePath
+ {
+ get
+ {
+ return Path.Combine(ConfigurationDirectoryPath, "system.xml");
+ }
+ }
+
+ /// <summary>
+ /// The _cache directory
+ /// </summary>
+ private string _cachePath;
+ /// <summary>
+ /// Gets the folder path to the cache directory
+ /// </summary>
+ /// <value>The cache directory.</value>
+ public string CachePath
+ {
+ get
+ {
+ if (string.IsNullOrEmpty(_cachePath))
+ {
+ _cachePath = Path.Combine(ProgramDataPath, "cache");
+
+ Directory.CreateDirectory(_cachePath);
+ }
+
+ return _cachePath;
+ }
+ set
+ {
+ _cachePath = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets the folder path to the temp directory within the cache folder
+ /// </summary>
+ /// <value>The temp directory.</value>
+ public string TempDirectory
+ {
+ get
+ {
+ return Path.Combine(CachePath, "temp");
+ }
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/Configuration/BaseConfigurationManager.cs b/Emby.Common.Implementations/Configuration/BaseConfigurationManager.cs
new file mode 100644
index 0000000000..27c9fe6157
--- /dev/null
+++ b/Emby.Common.Implementations/Configuration/BaseConfigurationManager.cs
@@ -0,0 +1,329 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Events;
+using MediaBrowser.Common.Extensions;
+using Emby.Common.Implementations;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Serialization;
+
+namespace Emby.Common.Implementations.Configuration
+{
+ /// <summary>
+ /// Class BaseConfigurationManager
+ /// </summary>
+ public abstract class BaseConfigurationManager : IConfigurationManager
+ {
+ /// <summary>
+ /// Gets the type of the configuration.
+ /// </summary>
+ /// <value>The type of the configuration.</value>
+ protected abstract Type ConfigurationType { get; }
+
+ /// <summary>
+ /// Occurs when [configuration updated].
+ /// </summary>
+ public event EventHandler<EventArgs> ConfigurationUpdated;
+
+ /// <summary>
+ /// Occurs when [configuration updating].
+ /// </summary>
+ public event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdating;
+
+ /// <summary>
+ /// Occurs when [named configuration updated].
+ /// </summary>
+ public event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdated;
+
+ /// <summary>
+ /// Gets the logger.
+ /// </summary>
+ /// <value>The logger.</value>
+ protected ILogger Logger { get; private set; }
+ /// <summary>
+ /// Gets the XML serializer.
+ /// </summary>
+ /// <value>The XML serializer.</value>
+ protected IXmlSerializer XmlSerializer { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the application paths.
+ /// </summary>
+ /// <value>The application paths.</value>
+ public IApplicationPaths CommonApplicationPaths { get; private set; }
+ public readonly IFileSystem FileSystem;
+
+ /// <summary>
+ /// The _configuration loaded
+ /// </summary>
+ private bool _configurationLoaded;
+ /// <summary>
+ /// The _configuration sync lock
+ /// </summary>
+ private object _configurationSyncLock = new object();
+ /// <summary>
+ /// The _configuration
+ /// </summary>
+ private BaseApplicationConfiguration _configuration;
+ /// <summary>
+ /// Gets the system configuration
+ /// </summary>
+ /// <value>The configuration.</value>
+ public BaseApplicationConfiguration CommonConfiguration
+ {
+ get
+ {
+ // Lazy load
+ LazyInitializer.EnsureInitialized(ref _configuration, ref _configurationLoaded, ref _configurationSyncLock, () => (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer, FileSystem));
+ return _configuration;
+ }
+ protected set
+ {
+ _configuration = value;
+
+ _configurationLoaded = value != null;
+ }
+ }
+
+ private ConfigurationStore[] _configurationStores = { };
+ private IConfigurationFactory[] _configurationFactories = { };
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BaseConfigurationManager" /> class.
+ /// </summary>
+ /// <param name="applicationPaths">The application paths.</param>
+ /// <param name="logManager">The log manager.</param>
+ /// <param name="xmlSerializer">The XML serializer.</param>
+ protected BaseConfigurationManager(IApplicationPaths applicationPaths, ILogManager logManager, IXmlSerializer xmlSerializer, IFileSystem fileSystem)
+ {
+ CommonApplicationPaths = applicationPaths;
+ XmlSerializer = xmlSerializer;
+ FileSystem = fileSystem;
+ Logger = logManager.GetLogger(GetType().Name);
+
+ UpdateCachePath();
+ }
+
+ public virtual void AddParts(IEnumerable<IConfigurationFactory> factories)
+ {
+ _configurationFactories = factories.ToArray();
+
+ _configurationStores = _configurationFactories
+ .SelectMany(i => i.GetConfigurations())
+ .ToArray();
+ }
+
+ /// <summary>
+ /// Saves the configuration.
+ /// </summary>
+ public void SaveConfiguration()
+ {
+ Logger.Info("Saving system configuration");
+ var path = CommonApplicationPaths.SystemConfigurationFilePath;
+
+ FileSystem.CreateDirectory(Path.GetDirectoryName(path));
+
+ lock (_configurationSyncLock)
+ {
+ XmlSerializer.SerializeToFile(CommonConfiguration, path);
+ }
+
+ OnConfigurationUpdated();
+ }
+
+ /// <summary>
+ /// Called when [configuration updated].
+ /// </summary>
+ protected virtual void OnConfigurationUpdated()
+ {
+ UpdateCachePath();
+
+ EventHelper.QueueEventIfNotNull(ConfigurationUpdated, this, EventArgs.Empty, Logger);
+ }
+
+ /// <summary>
+ /// Replaces the configuration.
+ /// </summary>
+ /// <param name="newConfiguration">The new configuration.</param>
+ /// <exception cref="System.ArgumentNullException">newConfiguration</exception>
+ public virtual void ReplaceConfiguration(BaseApplicationConfiguration newConfiguration)
+ {
+ if (newConfiguration == null)
+ {
+ throw new ArgumentNullException("newConfiguration");
+ }
+
+ ValidateCachePath(newConfiguration);
+
+ CommonConfiguration = newConfiguration;
+ SaveConfiguration();
+ }
+
+ /// <summary>
+ /// Updates the items by name path.
+ /// </summary>
+ private void UpdateCachePath()
+ {
+ string cachePath;
+
+ if (string.IsNullOrWhiteSpace(CommonConfiguration.CachePath))
+ {
+ cachePath = null;
+ }
+ else
+ {
+ cachePath = Path.Combine(CommonConfiguration.CachePath, "cache");
+ }
+
+ ((BaseApplicationPaths)CommonApplicationPaths).CachePath = cachePath;
+ }
+
+ /// <summary>
+ /// Replaces the cache path.
+ /// </summary>
+ /// <param name="newConfig">The new configuration.</param>
+ /// <exception cref="System.IO.DirectoryNotFoundException"></exception>
+ private void ValidateCachePath(BaseApplicationConfiguration newConfig)
+ {
+ var newPath = newConfig.CachePath;
+
+ if (!string.IsNullOrWhiteSpace(newPath)
+ && !string.Equals(CommonConfiguration.CachePath ?? string.Empty, newPath))
+ {
+ // Validate
+ if (!FileSystem.DirectoryExists(newPath))
+ {
+ throw new FileNotFoundException(string.Format("{0} does not exist.", newPath));
+ }
+
+ EnsureWriteAccess(newPath);
+ }
+ }
+
+ protected void EnsureWriteAccess(string path)
+ {
+ var file = Path.Combine(path, Guid.NewGuid().ToString());
+
+ FileSystem.WriteAllText(file, string.Empty);
+ FileSystem.DeleteFile(file);
+ }
+
+ private readonly ConcurrentDictionary<string, object> _configurations = new ConcurrentDictionary<string, object>();
+
+ private string GetConfigurationFile(string key)
+ {
+ return Path.Combine(CommonApplicationPaths.ConfigurationDirectoryPath, key.ToLower() + ".xml");
+ }
+
+ public object GetConfiguration(string key)
+ {
+ return _configurations.GetOrAdd(key, k =>
+ {
+ var file = GetConfigurationFile(key);
+
+ var configurationInfo = _configurationStores
+ .FirstOrDefault(i => string.Equals(i.Key, key, StringComparison.OrdinalIgnoreCase));
+
+ if (configurationInfo == null)
+ {
+ throw new ResourceNotFoundException("Configuration with key " + key + " not found.");
+ }
+
+ var configurationType = configurationInfo.ConfigurationType;
+
+ lock (_configurationSyncLock)
+ {
+ return LoadConfiguration(file, configurationType);
+ }
+ });
+ }
+
+ private object LoadConfiguration(string path, Type configurationType)
+ {
+ try
+ {
+ return XmlSerializer.DeserializeFromFile(configurationType, path);
+ }
+ catch (FileNotFoundException)
+ {
+ return Activator.CreateInstance(configurationType);
+ }
+ catch (IOException)
+ {
+ return Activator.CreateInstance(configurationType);
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error loading configuration file: {0}", ex, path);
+
+ return Activator.CreateInstance(configurationType);
+ }
+ }
+
+ public void SaveConfiguration(string key, object configuration)
+ {
+ var configurationStore = GetConfigurationStore(key);
+ var configurationType = configurationStore.ConfigurationType;
+
+ if (configuration.GetType() != configurationType)
+ {
+ throw new ArgumentException("Expected configuration type is " + configurationType.Name);
+ }
+
+ var validatingStore = configurationStore as IValidatingConfiguration;
+ if (validatingStore != null)
+ {
+ var currentConfiguration = GetConfiguration(key);
+
+ validatingStore.Validate(currentConfiguration, configuration);
+ }
+
+ EventHelper.FireEventIfNotNull(NamedConfigurationUpdating, this, new ConfigurationUpdateEventArgs
+ {
+ Key = key,
+ NewConfiguration = configuration
+
+ }, Logger);
+
+ _configurations.AddOrUpdate(key, configuration, (k, v) => configuration);
+
+ var path = GetConfigurationFile(key);
+ FileSystem.CreateDirectory(Path.GetDirectoryName(path));
+
+ lock (_configurationSyncLock)
+ {
+ XmlSerializer.SerializeToFile(configuration, path);
+ }
+
+ OnNamedConfigurationUpdated(key, configuration);
+ }
+
+ protected virtual void OnNamedConfigurationUpdated(string key, object configuration)
+ {
+ EventHelper.FireEventIfNotNull(NamedConfigurationUpdated, this, new ConfigurationUpdateEventArgs
+ {
+ Key = key,
+ NewConfiguration = configuration
+
+ }, Logger);
+ }
+
+ public Type GetConfigurationType(string key)
+ {
+ return GetConfigurationStore(key)
+ .ConfigurationType;
+ }
+
+ private ConfigurationStore GetConfigurationStore(string key)
+ {
+ return _configurationStores
+ .First(i => string.Equals(i.Key, key, StringComparison.OrdinalIgnoreCase));
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/Configuration/ConfigurationHelper.cs b/Emby.Common.Implementations/Configuration/ConfigurationHelper.cs
new file mode 100644
index 0000000000..0d43a651ea
--- /dev/null
+++ b/Emby.Common.Implementations/Configuration/ConfigurationHelper.cs
@@ -0,0 +1,60 @@
+using System;
+using System.IO;
+using System.Linq;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Serialization;
+
+namespace Emby.Common.Implementations.Configuration
+{
+ /// <summary>
+ /// Class ConfigurationHelper
+ /// </summary>
+ public static class ConfigurationHelper
+ {
+ /// <summary>
+ /// Reads an xml configuration file from the file system
+ /// It will immediately re-serialize and save if new serialization data is available due to property changes
+ /// </summary>
+ /// <param name="type">The type.</param>
+ /// <param name="path">The path.</param>
+ /// <param name="xmlSerializer">The XML serializer.</param>
+ /// <returns>System.Object.</returns>
+ public static object GetXmlConfiguration(Type type, string path, IXmlSerializer xmlSerializer, IFileSystem fileSystem)
+ {
+ object configuration;
+
+ byte[] buffer = null;
+
+ // Use try/catch to avoid the extra file system lookup using File.Exists
+ try
+ {
+ buffer = fileSystem.ReadAllBytes(path);
+
+ configuration = xmlSerializer.DeserializeFromBytes(type, buffer);
+ }
+ catch (Exception)
+ {
+ configuration = Activator.CreateInstance(type);
+ }
+
+ using (var stream = new MemoryStream())
+ {
+ xmlSerializer.SerializeToStream(configuration, stream);
+
+ // Take the object we just got and serialize it back to bytes
+ var newBytes = stream.ToArray();
+
+ // If the file didn't exist before, or if something has changed, re-save
+ if (buffer == null || !buffer.SequenceEqual(newBytes))
+ {
+ fileSystem.CreateDirectory(Path.GetDirectoryName(path));
+
+ // Save it after load in case we got new items
+ fileSystem.WriteAllBytes(path, newBytes);
+ }
+
+ return configuration;
+ }
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Common.Implementations/Cryptography/CryptographyProvider.cs
new file mode 100644
index 0000000000..01a31bcc03
--- /dev/null
+++ b/Emby.Common.Implementations/Cryptography/CryptographyProvider.cs
@@ -0,0 +1,40 @@
+using System;
+using System.IO;
+using System.Security.Cryptography;
+using System.Text;
+using MediaBrowser.Model.Cryptography;
+
+namespace Emby.Common.Implementations.Cryptography
+{
+ public class CryptographyProvider : ICryptoProvider
+ {
+ public Guid GetMD5(string str)
+ {
+ return new Guid(ComputeMD5(Encoding.Unicode.GetBytes(str)));
+ }
+
+ public byte[] ComputeSHA1(byte[] bytes)
+ {
+ using (var provider = SHA1.Create())
+ {
+ return provider.ComputeHash(bytes);
+ }
+ }
+
+ public byte[] ComputeMD5(Stream str)
+ {
+ using (var provider = MD5.Create())
+ {
+ return provider.ComputeHash(str);
+ }
+ }
+
+ public byte[] ComputeMD5(byte[] bytes)
+ {
+ using (var provider = MD5.Create())
+ {
+ return provider.ComputeHash(bytes);
+ }
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/Devices/DeviceId.cs b/Emby.Common.Implementations/Devices/DeviceId.cs
new file mode 100644
index 0000000000..3d23ab872b
--- /dev/null
+++ b/Emby.Common.Implementations/Devices/DeviceId.cs
@@ -0,0 +1,109 @@
+using System;
+using System.IO;
+using System.Text;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+
+namespace Emby.Common.Implementations.Devices
+{
+ public class DeviceId
+ {
+ private readonly IApplicationPaths _appPaths;
+ private readonly ILogger _logger;
+ private readonly IFileSystem _fileSystem;
+
+ private readonly object _syncLock = new object();
+
+ private string CachePath
+ {
+ get { return Path.Combine(_appPaths.DataPath, "device.txt"); }
+ }
+
+ private string GetCachedId()
+ {
+ try
+ {
+ lock (_syncLock)
+ {
+ var value = File.ReadAllText(CachePath, Encoding.UTF8);
+
+ Guid guid;
+ if (Guid.TryParse(value, out guid))
+ {
+ return value;
+ }
+
+ _logger.Error("Invalid value found in device id file");
+ }
+ }
+ catch (DirectoryNotFoundException)
+ {
+ }
+ catch (FileNotFoundException)
+ {
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error reading file", ex);
+ }
+
+ return null;
+ }
+
+ private void SaveId(string id)
+ {
+ try
+ {
+ var path = CachePath;
+
+ _fileSystem.CreateDirectory(Path.GetDirectoryName(path));
+
+ lock (_syncLock)
+ {
+ _fileSystem.WriteAllText(path, id, Encoding.UTF8);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error writing to file", ex);
+ }
+ }
+
+ private string GetNewId()
+ {
+ return Guid.NewGuid().ToString("N");
+ }
+
+ private string GetDeviceId()
+ {
+ var id = GetCachedId();
+
+ if (string.IsNullOrWhiteSpace(id))
+ {
+ id = GetNewId();
+ SaveId(id);
+ }
+
+ return id;
+ }
+
+ private string _id;
+
+ public DeviceId(IApplicationPaths appPaths, ILogger logger, IFileSystem fileSystem)
+ {
+ if (fileSystem == null) {
+ throw new ArgumentNullException ("fileSystem");
+ }
+
+ _appPaths = appPaths;
+ _logger = logger;
+ _fileSystem = fileSystem;
+ }
+
+ public string Value
+ {
+ get { return _id ?? (_id = GetDeviceId()); }
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/Diagnostics/CommonProcess.cs b/Emby.Common.Implementations/Diagnostics/CommonProcess.cs
new file mode 100644
index 0000000000..462345ced5
--- /dev/null
+++ b/Emby.Common.Implementations/Diagnostics/CommonProcess.cs
@@ -0,0 +1,108 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Diagnostics;
+
+namespace Emby.Common.Implementations.Diagnostics
+{
+ public class CommonProcess : IProcess
+ {
+ public event EventHandler Exited;
+
+ private readonly ProcessOptions _options;
+ private readonly Process _process;
+
+ public CommonProcess(ProcessOptions options)
+ {
+ _options = options;
+
+ var startInfo = new ProcessStartInfo
+ {
+ Arguments = options.Arguments,
+ FileName = options.FileName,
+ WorkingDirectory = options.WorkingDirectory,
+ UseShellExecute = options.UseShellExecute,
+ CreateNoWindow = options.CreateNoWindow,
+ RedirectStandardError = options.RedirectStandardError,
+ RedirectStandardInput = options.RedirectStandardInput,
+ RedirectStandardOutput = options.RedirectStandardOutput
+ };
+
+#if NET46
+ startInfo.ErrorDialog = options.ErrorDialog;
+
+ if (options.IsHidden)
+ {
+ startInfo.WindowStyle = ProcessWindowStyle.Hidden;
+ }
+#endif
+
+ _process = new Process
+ {
+ StartInfo = startInfo
+ };
+
+ if (options.EnableRaisingEvents)
+ {
+ _process.EnableRaisingEvents = true;
+ _process.Exited += _process_Exited;
+ }
+ }
+
+ private void _process_Exited(object sender, EventArgs e)
+ {
+ if (Exited != null)
+ {
+ Exited(this, e);
+ }
+ }
+
+ public ProcessOptions StartInfo
+ {
+ get { return _options; }
+ }
+
+ public StreamWriter StandardInput
+ {
+ get { return _process.StandardInput; }
+ }
+
+ public StreamReader StandardError
+ {
+ get { return _process.StandardError; }
+ }
+
+ public StreamReader StandardOutput
+ {
+ get { return _process.StandardOutput; }
+ }
+
+ public int ExitCode
+ {
+ get { return _process.ExitCode; }
+ }
+
+ public void Start()
+ {
+ _process.Start();
+ }
+
+ public void Kill()
+ {
+ _process.Kill();
+ }
+
+ public bool WaitForExit(int timeMs)
+ {
+ return _process.WaitForExit(timeMs);
+ }
+
+ public void Dispose()
+ {
+ _process.Dispose();
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/Diagnostics/ProcessFactory.cs b/Emby.Common.Implementations/Diagnostics/ProcessFactory.cs
new file mode 100644
index 0000000000..292da023c7
--- /dev/null
+++ b/Emby.Common.Implementations/Diagnostics/ProcessFactory.cs
@@ -0,0 +1,12 @@
+using MediaBrowser.Model.Diagnostics;
+
+namespace Emby.Common.Implementations.Diagnostics
+{
+ public class ProcessFactory : IProcessFactory
+ {
+ public IProcess Create(ProcessOptions options)
+ {
+ return new CommonProcess(options);
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/Emby.Common.Implementations.xproj b/Emby.Common.Implementations/Emby.Common.Implementations.xproj
new file mode 100644
index 0000000000..5bb6e4e589
--- /dev/null
+++ b/Emby.Common.Implementations/Emby.Common.Implementations.xproj
@@ -0,0 +1,23 @@
+<?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>5a27010a-09c6-4e86-93ea-437484c10917</ProjectGuid>
+ <RootNamespace>Emby.Common.Implementations</RootNamespace>
+ <BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
+ <OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
+ <TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
+ </PropertyGroup>
+ <PropertyGroup>
+ <SchemaVersion>2.0</SchemaVersion>
+ </PropertyGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
+ <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
+ </ItemGroup>
+ <Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
+</Project> \ No newline at end of file
diff --git a/Emby.Common.Implementations/EnvironmentInfo/EnvironmentInfo.cs b/Emby.Common.Implementations/EnvironmentInfo/EnvironmentInfo.cs
new file mode 100644
index 0000000000..6cc4626eaf
--- /dev/null
+++ b/Emby.Common.Implementations/EnvironmentInfo/EnvironmentInfo.cs
@@ -0,0 +1,119 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using MediaBrowser.Model.System;
+
+namespace Emby.Common.Implementations.EnvironmentInfo
+{
+ public class EnvironmentInfo : IEnvironmentInfo
+ {
+ public MediaBrowser.Model.System.Architecture? CustomArchitecture { get; set; }
+
+ public MediaBrowser.Model.System.OperatingSystem OperatingSystem
+ {
+ get
+ {
+#if NET46
+ switch (Environment.OSVersion.Platform)
+ {
+ case PlatformID.MacOSX:
+ return MediaBrowser.Model.System.OperatingSystem.OSX;
+ case PlatformID.Win32NT:
+ return MediaBrowser.Model.System.OperatingSystem.Windows;
+ case PlatformID.Unix:
+ return MediaBrowser.Model.System.OperatingSystem.Linux;
+ }
+#elif NETSTANDARD1_6
+ if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ return OperatingSystem.OSX;
+ }
+ if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ return OperatingSystem.Windows;
+ }
+ if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ return OperatingSystem.Linux;
+ }
+#endif
+ return MediaBrowser.Model.System.OperatingSystem.Windows;
+ }
+ }
+
+ public string OperatingSystemName
+ {
+ get
+ {
+#if NET46
+ return Environment.OSVersion.Platform.ToString();
+#elif NETSTANDARD1_6
+ return System.Runtime.InteropServices.RuntimeInformation.OSDescription;
+#endif
+ return "Operating System";
+ }
+ }
+
+ public string OperatingSystemVersion
+ {
+ get
+ {
+#if NET46
+ return Environment.OSVersion.Version.ToString() + " " + Environment.OSVersion.ServicePack.ToString();
+#elif NETSTANDARD1_6
+ return System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription;
+#endif
+ return "1.0";
+ }
+ }
+
+ public MediaBrowser.Model.System.Architecture SystemArchitecture
+ {
+ get
+ {
+ if (CustomArchitecture.HasValue)
+ {
+ return CustomArchitecture.Value;
+ }
+#if NET46
+ return Environment.Is64BitOperatingSystem ? MediaBrowser.Model.System.Architecture.X64 : MediaBrowser.Model.System.Architecture.X86;
+#elif NETSTANDARD1_6
+ switch(System.Runtime.InteropServices.RuntimeInformation.OSArchitecture)
+ {
+ case System.Runtime.InteropServices.Architecture.Arm:
+ return MediaBrowser.Model.System.Architecture.Arm;
+ case System.Runtime.InteropServices.Architecture.Arm64:
+ return MediaBrowser.Model.System.Architecture.Arm64;
+ case System.Runtime.InteropServices.Architecture.X64:
+ return MediaBrowser.Model.System.Architecture.X64;
+ case System.Runtime.InteropServices.Architecture.X86:
+ return MediaBrowser.Model.System.Architecture.X86;
+ }
+#endif
+ return MediaBrowser.Model.System.Architecture.X64;
+ }
+ }
+
+ public string GetEnvironmentVariable(string name)
+ {
+ return Environment.GetEnvironmentVariable(name);
+ }
+
+ public virtual string GetUserId()
+ {
+ return null;
+ }
+
+ public string StackTrace
+ {
+ get { return Environment.StackTrace; }
+ }
+
+ public void SetProcessEnvironmentVariable(string name, string value)
+ {
+ Environment.SetEnvironmentVariable(name, value);
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/HttpClientManager/HttpClientInfo.cs b/Emby.Common.Implementations/HttpClientManager/HttpClientInfo.cs
new file mode 100644
index 0000000000..ca481b33e7
--- /dev/null
+++ b/Emby.Common.Implementations/HttpClientManager/HttpClientInfo.cs
@@ -0,0 +1,16 @@
+using System;
+
+namespace Emby.Common.Implementations.HttpClientManager
+{
+ /// <summary>
+ /// Class HttpClientInfo
+ /// </summary>
+ public class HttpClientInfo
+ {
+ /// <summary>
+ /// Gets or sets the last timeout.
+ /// </summary>
+ /// <value>The last timeout.</value>
+ public DateTime LastTimeout { get; set; }
+ }
+}
diff --git a/Emby.Common.Implementations/HttpClientManager/HttpClientManager.cs b/Emby.Common.Implementations/HttpClientManager/HttpClientManager.cs
new file mode 100644
index 0000000000..06af5af536
--- /dev/null
+++ b/Emby.Common.Implementations/HttpClientManager/HttpClientManager.cs
@@ -0,0 +1,988 @@
+using System.Net.Sockets;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Net;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Emby.Common.Implementations.HttpClientManager;
+using MediaBrowser.Model.IO;
+
+namespace Emby.Common.Implementations.HttpClientManager
+{
+ /// <summary>
+ /// Class HttpClientManager
+ /// </summary>
+ public class HttpClientManager : IHttpClient
+ {
+ /// <summary>
+ /// When one request to a host times out, we'll ban all other requests for this period of time, to prevent scans from stalling
+ /// </summary>
+ private const int TimeoutSeconds = 30;
+
+ /// <summary>
+ /// The _logger
+ /// </summary>
+ private readonly ILogger _logger;
+
+ /// <summary>
+ /// The _app paths
+ /// </summary>
+ private readonly IApplicationPaths _appPaths;
+
+ private readonly IFileSystem _fileSystem;
+ private readonly IMemoryStreamFactory _memoryStreamProvider;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HttpClientManager" /> class.
+ /// </summary>
+ /// <param name="appPaths">The app paths.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="fileSystem">The file system.</param>
+ /// <exception cref="System.ArgumentNullException">appPaths
+ /// or
+ /// logger</exception>
+ public HttpClientManager(IApplicationPaths appPaths, ILogger logger, IFileSystem fileSystem, IMemoryStreamFactory memoryStreamProvider)
+ {
+ if (appPaths == null)
+ {
+ throw new ArgumentNullException("appPaths");
+ }
+ if (logger == null)
+ {
+ throw new ArgumentNullException("logger");
+ }
+
+ _logger = logger;
+ _fileSystem = fileSystem;
+ _memoryStreamProvider = memoryStreamProvider;
+ _appPaths = appPaths;
+
+#if NET46
+ // http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c
+ ServicePointManager.Expect100Continue = false;
+
+ // Trakt requests sometimes fail without this
+ ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3 | SecurityProtocolType.Tls;
+#endif
+ }
+
+ /// <summary>
+ /// Holds a dictionary of http clients by host. Use GetHttpClient(host) to retrieve or create a client for web requests.
+ /// DON'T dispose it after use.
+ /// </summary>
+ /// <value>The HTTP clients.</value>
+ private readonly ConcurrentDictionary<string, HttpClientInfo> _httpClients = new ConcurrentDictionary<string, HttpClientInfo>();
+
+ /// <summary>
+ /// Gets
+ /// </summary>
+ /// <param name="host">The host.</param>
+ /// <param name="enableHttpCompression">if set to <c>true</c> [enable HTTP compression].</param>
+ /// <returns>HttpClient.</returns>
+ /// <exception cref="System.ArgumentNullException">host</exception>
+ private HttpClientInfo GetHttpClient(string host, bool enableHttpCompression)
+ {
+ if (string.IsNullOrEmpty(host))
+ {
+ throw new ArgumentNullException("host");
+ }
+
+ HttpClientInfo client;
+
+ var key = host + enableHttpCompression;
+
+ if (!_httpClients.TryGetValue(key, out client))
+ {
+ client = new HttpClientInfo();
+
+ _httpClients.TryAdd(key, client);
+ }
+
+ return client;
+ }
+
+ private WebRequest CreateWebRequest(string url)
+ {
+ try
+ {
+ return WebRequest.Create(url);
+ }
+ catch (NotSupportedException)
+ {
+ //Webrequest creation does fail on MONO randomly when using WebRequest.Create
+ //the issue occurs in the GetCreator method here: http://www.oschina.net/code/explore/mono-2.8.1/mcs/class/System/System.Net/WebRequest.cs
+
+ var type = Type.GetType("System.Net.HttpRequestCreator, System, Version=4.0.0.0,Culture=neutral, PublicKeyToken=b77a5c561934e089");
+ var creator = Activator.CreateInstance(type, nonPublic: true) as IWebRequestCreate;
+ return creator.Create(new Uri(url)) as HttpWebRequest;
+ }
+ }
+
+ private void AddIpv4Option(HttpWebRequest request, HttpRequestOptions options)
+ {
+#if NET46
+ request.ServicePoint.BindIPEndPointDelegate = (servicePount, remoteEndPoint, retryCount) =>
+ {
+ if (remoteEndPoint.AddressFamily == AddressFamily.InterNetwork)
+ {
+ return new IPEndPoint(IPAddress.Any, 0);
+ }
+ throw new InvalidOperationException("no IPv4 address");
+ };
+#endif
+ }
+
+ private WebRequest GetRequest(HttpRequestOptions options, string method)
+ {
+ var url = options.Url;
+
+ var uriAddress = new Uri(url);
+ var userInfo = uriAddress.UserInfo;
+ if (!string.IsNullOrWhiteSpace(userInfo))
+ {
+ _logger.Info("Found userInfo in url: {0} ... url: {1}", userInfo, url);
+ url = url.Replace(userInfo + "@", string.Empty);
+ }
+
+ var request = CreateWebRequest(url);
+ var httpWebRequest = request as HttpWebRequest;
+
+ if (httpWebRequest != null)
+ {
+ if (options.PreferIpv4)
+ {
+ AddIpv4Option(httpWebRequest, options);
+ }
+
+ AddRequestHeaders(httpWebRequest, options);
+
+#if NET46
+ if (options.EnableHttpCompression)
+ {
+ if (options.DecompressionMethod.HasValue)
+ {
+ httpWebRequest.AutomaticDecompression = options.DecompressionMethod.Value == CompressionMethod.Gzip
+ ? DecompressionMethods.GZip
+ : DecompressionMethods.Deflate;
+ }
+ else
+ {
+ httpWebRequest.AutomaticDecompression = DecompressionMethods.Deflate;
+ }
+ }
+ else
+ {
+ httpWebRequest.AutomaticDecompression = DecompressionMethods.None;
+ }
+#endif
+ }
+
+
+
+#if NET46
+ request.CachePolicy = new System.Net.Cache.RequestCachePolicy(System.Net.Cache.RequestCacheLevel.BypassCache);
+#endif
+
+ if (httpWebRequest != null)
+ {
+ if (options.EnableKeepAlive)
+ {
+#if NET46
+ httpWebRequest.KeepAlive = true;
+#endif
+ }
+ }
+
+ request.Method = method;
+#if NET46
+ request.Timeout = options.TimeoutMs;
+#endif
+
+ if (httpWebRequest != null)
+ {
+ if (!string.IsNullOrEmpty(options.Host))
+ {
+#if NET46
+ httpWebRequest.Host = options.Host;
+#elif NETSTANDARD1_6
+ httpWebRequest.Headers["Host"] = options.Host;
+#endif
+ }
+
+ if (!string.IsNullOrEmpty(options.Referer))
+ {
+#if NET46
+ httpWebRequest.Referer = options.Referer;
+#elif NETSTANDARD1_6
+ httpWebRequest.Headers["Referer"] = options.Referer;
+#endif
+ }
+ }
+
+ if (!string.IsNullOrWhiteSpace(userInfo))
+ {
+ var parts = userInfo.Split(':');
+ if (parts.Length == 2)
+ {
+ request.Credentials = GetCredential(url, parts[0], parts[1]);
+ // TODO: .net core ??
+#if NET46
+ request.PreAuthenticate = true;
+#endif
+ }
+ }
+
+ return request;
+ }
+
+ private CredentialCache GetCredential(string url, string username, string password)
+ {
+ //ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3;
+ CredentialCache credentialCache = new CredentialCache();
+ credentialCache.Add(new Uri(url), "Basic", new NetworkCredential(username, password));
+ return credentialCache;
+ }
+
+ private void AddRequestHeaders(HttpWebRequest request, HttpRequestOptions options)
+ {
+ foreach (var header in options.RequestHeaders.ToList())
+ {
+ if (string.Equals(header.Key, "Accept", StringComparison.OrdinalIgnoreCase))
+ {
+ request.Accept = header.Value;
+ }
+ else if (string.Equals(header.Key, "User-Agent", StringComparison.OrdinalIgnoreCase))
+ {
+#if NET46
+ request.UserAgent = header.Value;
+#elif NETSTANDARD1_6
+ request.Headers["User-Agent"] = header.Value;
+#endif
+ }
+ else
+ {
+#if NET46
+ request.Headers.Set(header.Key, header.Value);
+#elif NETSTANDARD1_6
+ request.Headers[header.Key] = header.Value;
+#endif
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets the response internal.
+ /// </summary>
+ /// <param name="options">The options.</param>
+ /// <returns>Task{HttpResponseInfo}.</returns>
+ public Task<HttpResponseInfo> GetResponse(HttpRequestOptions options)
+ {
+ return SendAsync(options, "GET");
+ }
+
+ /// <summary>
+ /// Performs a GET request and returns the resulting stream
+ /// </summary>
+ /// <param name="options">The options.</param>
+ /// <returns>Task{Stream}.</returns>
+ public async Task<Stream> Get(HttpRequestOptions options)
+ {
+ var response = await GetResponse(options).ConfigureAwait(false);
+
+ return response.Content;
+ }
+
+ /// <summary>
+ /// Performs a GET request and returns the resulting stream
+ /// </summary>
+ /// <param name="url">The URL.</param>
+ /// <param name="resourcePool">The resource pool.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{Stream}.</returns>
+ public Task<Stream> Get(string url, SemaphoreSlim resourcePool, CancellationToken cancellationToken)
+ {
+ return Get(new HttpRequestOptions
+ {
+ Url = url,
+ ResourcePool = resourcePool,
+ CancellationToken = cancellationToken,
+ BufferContent = resourcePool != null
+ });
+ }
+
+ /// <summary>
+ /// Gets the specified URL.
+ /// </summary>
+ /// <param name="url">The URL.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{Stream}.</returns>
+ public Task<Stream> Get(string url, CancellationToken cancellationToken)
+ {
+ return Get(url, null, cancellationToken);
+ }
+
+ /// <summary>
+ /// send as an asynchronous operation.
+ /// </summary>
+ /// <param name="options">The options.</param>
+ /// <param name="httpMethod">The HTTP method.</param>
+ /// <returns>Task{HttpResponseInfo}.</returns>
+ /// <exception cref="HttpException">
+ /// </exception>
+ public async Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, string httpMethod)
+ {
+ if (options.CacheMode == CacheMode.None)
+ {
+ return await SendAsyncInternal(options, httpMethod).ConfigureAwait(false);
+ }
+
+ var url = options.Url;
+ var urlHash = url.ToLower().GetMD5().ToString("N");
+
+ var responseCachePath = Path.Combine(_appPaths.CachePath, "httpclient", urlHash);
+
+ var response = await GetCachedResponse(responseCachePath, options.CacheLength, url).ConfigureAwait(false);
+ if (response != null)
+ {
+ return response;
+ }
+
+ response = await SendAsyncInternal(options, httpMethod).ConfigureAwait(false);
+
+ if (response.StatusCode == HttpStatusCode.OK)
+ {
+ await CacheResponse(response, responseCachePath).ConfigureAwait(false);
+ }
+
+ return response;
+ }
+
+ private async Task<HttpResponseInfo> GetCachedResponse(string responseCachePath, TimeSpan cacheLength, string url)
+ {
+ _logger.Info("Checking for cache file {0}", responseCachePath);
+
+ try
+ {
+ if (_fileSystem.GetLastWriteTimeUtc(responseCachePath).Add(cacheLength) > DateTime.UtcNow)
+ {
+ using (var stream = _fileSystem.GetFileStream(responseCachePath, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read, true))
+ {
+ var memoryStream = _memoryStreamProvider.CreateNew();
+
+ await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
+ memoryStream.Position = 0;
+
+ return new HttpResponseInfo
+ {
+ ResponseUrl = url,
+ Content = memoryStream,
+ StatusCode = HttpStatusCode.OK,
+ ContentLength = memoryStream.Length
+ };
+ }
+ }
+ }
+ catch (FileNotFoundException)
+ {
+
+ }
+ catch (DirectoryNotFoundException)
+ {
+
+ }
+
+ return null;
+ }
+
+ private async Task CacheResponse(HttpResponseInfo response, string responseCachePath)
+ {
+ _fileSystem.CreateDirectory(Path.GetDirectoryName(responseCachePath));
+
+ using (var responseStream = response.Content)
+ {
+ var memoryStream = _memoryStreamProvider.CreateNew();
+ await responseStream.CopyToAsync(memoryStream).ConfigureAwait(false);
+ memoryStream.Position = 0;
+
+ using (var fileStream = _fileSystem.GetFileStream(responseCachePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.None, true))
+ {
+ await memoryStream.CopyToAsync(fileStream).ConfigureAwait(false);
+
+ memoryStream.Position = 0;
+ response.Content = memoryStream;
+ }
+ }
+ }
+
+ private async Task<HttpResponseInfo> SendAsyncInternal(HttpRequestOptions options, string httpMethod)
+ {
+ ValidateParams(options);
+
+ options.CancellationToken.ThrowIfCancellationRequested();
+
+ var client = GetHttpClient(GetHostFromUrl(options.Url), options.EnableHttpCompression);
+
+ if ((DateTime.UtcNow - client.LastTimeout).TotalSeconds < TimeoutSeconds)
+ {
+ throw new HttpException(string.Format("Cancelling connection to {0} due to a previous timeout.", options.Url))
+ {
+ IsTimedOut = true
+ };
+ }
+
+ var httpWebRequest = GetRequest(options, httpMethod);
+
+ if (options.RequestContentBytes != null ||
+ !string.IsNullOrEmpty(options.RequestContent) ||
+ string.Equals(httpMethod, "post", StringComparison.OrdinalIgnoreCase))
+ {
+ var bytes = options.RequestContentBytes ??
+ Encoding.UTF8.GetBytes(options.RequestContent ?? string.Empty);
+
+ httpWebRequest.ContentType = options.RequestContentType ?? "application/x-www-form-urlencoded";
+
+#if NET46
+ httpWebRequest.ContentLength = bytes.Length;
+#endif
+ (await httpWebRequest.GetRequestStreamAsync().ConfigureAwait(false)).Write(bytes, 0, bytes.Length);
+ }
+
+ if (options.ResourcePool != null)
+ {
+ await options.ResourcePool.WaitAsync(options.CancellationToken).ConfigureAwait(false);
+ }
+
+ if ((DateTime.UtcNow - client.LastTimeout).TotalSeconds < TimeoutSeconds)
+ {
+ if (options.ResourcePool != null)
+ {
+ options.ResourcePool.Release();
+ }
+
+ throw new HttpException(string.Format("Connection to {0} timed out", options.Url)) { IsTimedOut = true };
+ }
+
+ if (options.LogRequest)
+ {
+ _logger.Info("HttpClientManager {0}: {1}", httpMethod.ToUpper(), options.Url);
+ }
+
+ try
+ {
+ options.CancellationToken.ThrowIfCancellationRequested();
+
+ if (!options.BufferContent)
+ {
+ var response = await GetResponseAsync(httpWebRequest, TimeSpan.FromMilliseconds(options.TimeoutMs)).ConfigureAwait(false);
+
+ var httpResponse = (HttpWebResponse)response;
+
+ EnsureSuccessStatusCode(client, httpResponse, options);
+
+ options.CancellationToken.ThrowIfCancellationRequested();
+
+ return GetResponseInfo(httpResponse, httpResponse.GetResponseStream(), GetContentLength(httpResponse), httpResponse);
+ }
+
+ using (var response = await GetResponseAsync(httpWebRequest, TimeSpan.FromMilliseconds(options.TimeoutMs)).ConfigureAwait(false))
+ {
+ var httpResponse = (HttpWebResponse)response;
+
+ EnsureSuccessStatusCode(client, httpResponse, options);
+
+ options.CancellationToken.ThrowIfCancellationRequested();
+
+ using (var stream = httpResponse.GetResponseStream())
+ {
+ var memoryStream = _memoryStreamProvider.CreateNew();
+
+ await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
+
+ memoryStream.Position = 0;
+
+ return GetResponseInfo(httpResponse, memoryStream, memoryStream.Length, null);
+ }
+ }
+ }
+ catch (OperationCanceledException ex)
+ {
+ throw GetCancellationException(options, client, options.CancellationToken, ex);
+ }
+ catch (Exception ex)
+ {
+ throw GetException(ex, options, client);
+ }
+ finally
+ {
+ if (options.ResourcePool != null)
+ {
+ options.ResourcePool.Release();
+ }
+ }
+ }
+
+ private HttpResponseInfo GetResponseInfo(HttpWebResponse httpResponse, Stream content, long? contentLength, IDisposable disposable)
+ {
+ var responseInfo = new HttpResponseInfo(disposable)
+ {
+ Content = content,
+
+ StatusCode = httpResponse.StatusCode,
+
+ ContentType = httpResponse.ContentType,
+
+ ContentLength = contentLength,
+
+ ResponseUrl = httpResponse.ResponseUri.ToString()
+ };
+
+ if (httpResponse.Headers != null)
+ {
+ SetHeaders(httpResponse.Headers, responseInfo);
+ }
+
+ return responseInfo;
+ }
+
+ private HttpResponseInfo GetResponseInfo(HttpWebResponse httpResponse, string tempFile, long? contentLength)
+ {
+ var responseInfo = new HttpResponseInfo
+ {
+ TempFilePath = tempFile,
+
+ StatusCode = httpResponse.StatusCode,
+
+ ContentType = httpResponse.ContentType,
+
+ ContentLength = contentLength
+ };
+
+ if (httpResponse.Headers != null)
+ {
+ SetHeaders(httpResponse.Headers, responseInfo);
+ }
+
+ return responseInfo;
+ }
+
+ private void SetHeaders(WebHeaderCollection headers, HttpResponseInfo responseInfo)
+ {
+ foreach (var key in headers.AllKeys)
+ {
+ responseInfo.Headers[key] = headers[key];
+ }
+ }
+
+ public Task<HttpResponseInfo> Post(HttpRequestOptions options)
+ {
+ return SendAsync(options, "POST");
+ }
+
+ /// <summary>
+ /// Performs a POST request
+ /// </summary>
+ /// <param name="options">The options.</param>
+ /// <param name="postData">Params to add to the POST data.</param>
+ /// <returns>stream on success, null on failure</returns>
+ public async Task<Stream> Post(HttpRequestOptions options, Dictionary<string, string> postData)
+ {
+ options.SetPostData(postData);
+
+ var response = await Post(options).ConfigureAwait(false);
+
+ return response.Content;
+ }
+
+ /// <summary>
+ /// Performs a POST request
+ /// </summary>
+ /// <param name="url">The URL.</param>
+ /// <param name="postData">Params to add to the POST data.</param>
+ /// <param name="resourcePool">The resource pool.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>stream on success, null on failure</returns>
+ public Task<Stream> Post(string url, Dictionary<string, string> postData, SemaphoreSlim resourcePool, CancellationToken cancellationToken)
+ {
+ return Post(new HttpRequestOptions
+ {
+ Url = url,
+ ResourcePool = resourcePool,
+ CancellationToken = cancellationToken,
+ BufferContent = resourcePool != null
+
+ }, postData);
+ }
+
+ /// <summary>
+ /// Downloads the contents of a given url into a temporary location
+ /// </summary>
+ /// <param name="options">The options.</param>
+ /// <returns>Task{System.String}.</returns>
+ public async Task<string> GetTempFile(HttpRequestOptions options)
+ {
+ var response = await GetTempFileResponse(options).ConfigureAwait(false);
+
+ return response.TempFilePath;
+ }
+
+ public async Task<HttpResponseInfo> GetTempFileResponse(HttpRequestOptions options)
+ {
+ ValidateParams(options);
+
+ _fileSystem.CreateDirectory(_appPaths.TempDirectory);
+
+ var tempFile = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + ".tmp");
+
+ if (options.Progress == null)
+ {
+ throw new ArgumentNullException("progress");
+ }
+
+ options.CancellationToken.ThrowIfCancellationRequested();
+
+ var httpWebRequest = GetRequest(options, "GET");
+
+ if (options.ResourcePool != null)
+ {
+ await options.ResourcePool.WaitAsync(options.CancellationToken).ConfigureAwait(false);
+ }
+
+ options.Progress.Report(0);
+
+ if (options.LogRequest)
+ {
+ _logger.Info("HttpClientManager.GetTempFileResponse url: {0}", options.Url);
+ }
+
+ var client = GetHttpClient(GetHostFromUrl(options.Url), options.EnableHttpCompression);
+
+ try
+ {
+ options.CancellationToken.ThrowIfCancellationRequested();
+
+ using (var response = await httpWebRequest.GetResponseAsync().ConfigureAwait(false))
+ {
+ var httpResponse = (HttpWebResponse)response;
+
+ EnsureSuccessStatusCode(client, httpResponse, options);
+
+ options.CancellationToken.ThrowIfCancellationRequested();
+
+ var contentLength = GetContentLength(httpResponse);
+
+ if (!contentLength.HasValue)
+ {
+ // We're not able to track progress
+ using (var stream = httpResponse.GetResponseStream())
+ {
+ using (var fs = _fileSystem.GetFileStream(tempFile, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true))
+ {
+ await stream.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, options.CancellationToken).ConfigureAwait(false);
+ }
+ }
+ }
+ else
+ {
+ using (var stream = ProgressStream.CreateReadProgressStream(httpResponse.GetResponseStream(), options.Progress.Report, contentLength.Value))
+ {
+ using (var fs = _fileSystem.GetFileStream(tempFile, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true))
+ {
+ await stream.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, options.CancellationToken).ConfigureAwait(false);
+ }
+ }
+ }
+
+ options.Progress.Report(100);
+
+ return GetResponseInfo(httpResponse, tempFile, contentLength);
+ }
+ }
+ catch (Exception ex)
+ {
+ DeleteTempFile(tempFile);
+ throw GetException(ex, options, client);
+ }
+ finally
+ {
+ if (options.ResourcePool != null)
+ {
+ options.ResourcePool.Release();
+ }
+ }
+ }
+
+ private long? GetContentLength(HttpWebResponse response)
+ {
+ var length = response.ContentLength;
+
+ if (length == 0)
+ {
+ return null;
+ }
+
+ return length;
+ }
+
+ protected static readonly CultureInfo UsCulture = new CultureInfo("en-US");
+
+ private Exception GetException(Exception ex, HttpRequestOptions options, HttpClientInfo client)
+ {
+ if (ex is HttpException)
+ {
+ return ex;
+ }
+
+ var webException = ex as WebException
+ ?? ex.InnerException as WebException;
+
+ if (webException != null)
+ {
+ if (options.LogErrors)
+ {
+ _logger.ErrorException("Error getting response from " + options.Url, ex);
+ }
+
+ var exception = new HttpException(ex.Message, ex);
+
+ var response = webException.Response as HttpWebResponse;
+ if (response != null)
+ {
+ exception.StatusCode = response.StatusCode;
+
+ if ((int)response.StatusCode == 429)
+ {
+ client.LastTimeout = DateTime.UtcNow;
+ }
+ }
+
+ return exception;
+ }
+
+ var operationCanceledException = ex as OperationCanceledException
+ ?? ex.InnerException as OperationCanceledException;
+
+ if (operationCanceledException != null)
+ {
+ return GetCancellationException(options, client, options.CancellationToken, operationCanceledException);
+ }
+
+ if (options.LogErrors)
+ {
+ _logger.ErrorException("Error getting response from " + options.Url, ex);
+ }
+
+ return ex;
+ }
+
+ private void DeleteTempFile(string file)
+ {
+ try
+ {
+ _fileSystem.DeleteFile(file);
+ }
+ catch (IOException)
+ {
+ // Might not have been created at all. No need to worry.
+ }
+ }
+
+ private void ValidateParams(HttpRequestOptions options)
+ {
+ if (string.IsNullOrEmpty(options.Url))
+ {
+ throw new ArgumentNullException("options");
+ }
+ }
+
+ /// <summary>
+ /// Gets the host from URL.
+ /// </summary>
+ /// <param name="url">The URL.</param>
+ /// <returns>System.String.</returns>
+ private string GetHostFromUrl(string url)
+ {
+ var index = url.IndexOf("://", StringComparison.OrdinalIgnoreCase);
+
+ if (index != -1)
+ {
+ url = url.Substring(index + 3);
+ var host = url.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
+
+ if (!string.IsNullOrWhiteSpace(host))
+ {
+ return host;
+ }
+ }
+
+ return url;
+ }
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources.
+ /// </summary>
+ /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool dispose)
+ {
+ if (dispose)
+ {
+ _httpClients.Clear();
+ }
+ }
+
+ /// <summary>
+ /// Throws the cancellation exception.
+ /// </summary>
+ /// <param name="options">The options.</param>
+ /// <param name="client">The client.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="exception">The exception.</param>
+ /// <returns>Exception.</returns>
+ private Exception GetCancellationException(HttpRequestOptions options, HttpClientInfo client, CancellationToken cancellationToken, OperationCanceledException exception)
+ {
+ // If the HttpClient's timeout is reached, it will cancel the Task internally
+ if (!cancellationToken.IsCancellationRequested)
+ {
+ var msg = string.Format("Connection to {0} timed out", options.Url);
+
+ if (options.LogErrors)
+ {
+ _logger.Error(msg);
+ }
+
+ client.LastTimeout = DateTime.UtcNow;
+
+ // Throw an HttpException so that the caller doesn't think it was cancelled by user code
+ return new HttpException(msg, exception)
+ {
+ IsTimedOut = true
+ };
+ }
+
+ return exception;
+ }
+
+ private void EnsureSuccessStatusCode(HttpClientInfo client, HttpWebResponse response, HttpRequestOptions options)
+ {
+ var statusCode = response.StatusCode;
+
+ var isSuccessful = statusCode >= HttpStatusCode.OK && statusCode <= (HttpStatusCode)299;
+
+ if (!isSuccessful)
+ {
+ if (options.LogErrorResponseBody)
+ {
+ try
+ {
+ using (var stream = response.GetResponseStream())
+ {
+ if (stream != null)
+ {
+ using (var reader = new StreamReader(stream))
+ {
+ var msg = reader.ReadToEnd();
+
+ _logger.Error(msg);
+ }
+ }
+ }
+ }
+ catch
+ {
+
+ }
+ }
+ throw new HttpException(response.StatusDescription)
+ {
+ StatusCode = response.StatusCode
+ };
+ }
+ }
+
+ /// <summary>
+ /// Posts the specified URL.
+ /// </summary>
+ /// <param name="url">The URL.</param>
+ /// <param name="postData">The post data.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{Stream}.</returns>
+ public Task<Stream> Post(string url, Dictionary<string, string> postData, CancellationToken cancellationToken)
+ {
+ return Post(url, postData, null, cancellationToken);
+ }
+
+ private Task<WebResponse> GetResponseAsync(WebRequest request, TimeSpan timeout)
+ {
+#if NET46
+ var taskCompletion = new TaskCompletionSource<WebResponse>();
+
+ Task<WebResponse> asyncTask = Task.Factory.FromAsync<WebResponse>(request.BeginGetResponse, request.EndGetResponse, null);
+
+ ThreadPool.RegisterWaitForSingleObject((asyncTask as IAsyncResult).AsyncWaitHandle, TimeoutCallback, request, timeout, true);
+ var callback = new TaskCallback { taskCompletion = taskCompletion };
+ asyncTask.ContinueWith(callback.OnSuccess, TaskContinuationOptions.NotOnFaulted);
+
+ // Handle errors
+ asyncTask.ContinueWith(callback.OnError, TaskContinuationOptions.OnlyOnFaulted);
+
+ return taskCompletion.Task;
+#endif
+
+ return request.GetResponseAsync();
+ }
+
+ private static void TimeoutCallback(object state, bool timedOut)
+ {
+ if (timedOut)
+ {
+ WebRequest request = (WebRequest)state;
+ if (state != null)
+ {
+ request.Abort();
+ }
+ }
+ }
+
+ private class TaskCallback
+ {
+ public TaskCompletionSource<WebResponse> taskCompletion;
+
+ public void OnSuccess(Task<WebResponse> task)
+ {
+ taskCompletion.TrySetResult(task.Result);
+ }
+
+ public void OnError(Task<WebResponse> task)
+ {
+ if (task.Exception != null)
+ {
+ taskCompletion.TrySetException(task.Exception);
+ }
+ else
+ {
+ taskCompletion.TrySetException(new List<Exception>());
+ }
+ }
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/IO/IsoManager.cs b/Emby.Common.Implementations/IO/IsoManager.cs
new file mode 100644
index 0000000000..14614acaf8
--- /dev/null
+++ b/Emby.Common.Implementations/IO/IsoManager.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.IO;
+
+namespace Emby.Common.Implementations.IO
+{
+ /// <summary>
+ /// Class IsoManager
+ /// </summary>
+ public class IsoManager : IIsoManager
+ {
+ /// <summary>
+ /// The _mounters
+ /// </summary>
+ private readonly List<IIsoMounter> _mounters = new List<IIsoMounter>();
+
+ /// <summary>
+ /// Mounts the specified iso path.
+ /// </summary>
+ /// <param name="isoPath">The iso path.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>IsoMount.</returns>
+ /// <exception cref="System.ArgumentNullException">isoPath</exception>
+ /// <exception cref="System.ArgumentException"></exception>
+ public Task<IIsoMount> Mount(string isoPath, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(isoPath))
+ {
+ throw new ArgumentNullException("isoPath");
+ }
+
+ var mounter = _mounters.FirstOrDefault(i => i.CanMount(isoPath));
+
+ if (mounter == null)
+ {
+ throw new ArgumentException(string.Format("No mounters are able to mount {0}", isoPath));
+ }
+
+ return mounter.Mount(isoPath, cancellationToken);
+ }
+
+ /// <summary>
+ /// Determines whether this instance can mount the specified path.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns><c>true</c> if this instance can mount the specified path; otherwise, <c>false</c>.</returns>
+ public bool CanMount(string path)
+ {
+ return _mounters.Any(i => i.CanMount(path));
+ }
+
+ /// <summary>
+ /// Adds the parts.
+ /// </summary>
+ /// <param name="mounters">The mounters.</param>
+ public void AddParts(IEnumerable<IIsoMounter> mounters)
+ {
+ _mounters.AddRange(mounters);
+ }
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose()
+ {
+ foreach (var mounter in _mounters)
+ {
+ mounter.Dispose();
+ }
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/IO/ManagedFileSystem.cs b/Emby.Common.Implementations/IO/ManagedFileSystem.cs
new file mode 100644
index 0000000000..62d285072c
--- /dev/null
+++ b/Emby.Common.Implementations/IO/ManagedFileSystem.cs
@@ -0,0 +1,794 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+
+namespace Emby.Common.Implementations.IO
+{
+ /// <summary>
+ /// Class ManagedFileSystem
+ /// </summary>
+ public class ManagedFileSystem : IFileSystem
+ {
+ protected ILogger Logger;
+
+ private readonly bool _supportsAsyncFileStreams;
+ private char[] _invalidFileNameChars;
+ private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
+ private bool EnableFileSystemRequestConcat = true;
+
+ public ManagedFileSystem(ILogger logger, bool supportsAsyncFileStreams, bool enableManagedInvalidFileNameChars, bool enableFileSystemRequestConcat)
+ {
+ Logger = logger;
+ _supportsAsyncFileStreams = supportsAsyncFileStreams;
+ EnableFileSystemRequestConcat = enableFileSystemRequestConcat;
+ SetInvalidFileNameChars(enableManagedInvalidFileNameChars);
+ }
+
+ public void AddShortcutHandler(IShortcutHandler handler)
+ {
+ _shortcutHandlers.Add(handler);
+ }
+
+ protected void SetInvalidFileNameChars(bool enableManagedInvalidFileNameChars)
+ {
+ if (enableManagedInvalidFileNameChars)
+ {
+ _invalidFileNameChars = Path.GetInvalidFileNameChars();
+ }
+ else
+ {
+ // GetInvalidFileNameChars is less restrictive in Linux/Mac than Windows, this mimic Windows behavior for mono under Linux/Mac.
+ _invalidFileNameChars = new char[41] { '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07',
+ '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', '\x10', '\x11', '\x12',
+ '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D',
+ '\x1E', '\x1F', '\x22', '\x3C', '\x3E', '\x7C', ':', '*', '?', '\\', '/' };
+ }
+ }
+
+ public char DirectorySeparatorChar
+ {
+ get
+ {
+ return Path.DirectorySeparatorChar;
+ }
+ }
+
+ public char PathSeparator
+ {
+ get
+ {
+ return Path.PathSeparator;
+ }
+ }
+
+ public string GetFullPath(string path)
+ {
+ return Path.GetFullPath(path);
+ }
+
+ /// <summary>
+ /// Determines whether the specified filename is shortcut.
+ /// </summary>
+ /// <param name="filename">The filename.</param>
+ /// <returns><c>true</c> if the specified filename is shortcut; otherwise, <c>false</c>.</returns>
+ /// <exception cref="System.ArgumentNullException">filename</exception>
+ public virtual bool IsShortcut(string filename)
+ {
+ if (string.IsNullOrEmpty(filename))
+ {
+ throw new ArgumentNullException("filename");
+ }
+
+ var extension = Path.GetExtension(filename);
+ return _shortcutHandlers.Any(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase));
+ }
+
+ /// <summary>
+ /// Resolves the shortcut.
+ /// </summary>
+ /// <param name="filename">The filename.</param>
+ /// <returns>System.String.</returns>
+ /// <exception cref="System.ArgumentNullException">filename</exception>
+ public virtual string ResolveShortcut(string filename)
+ {
+ if (string.IsNullOrEmpty(filename))
+ {
+ throw new ArgumentNullException("filename");
+ }
+
+ var extension = Path.GetExtension(filename);
+ var handler = _shortcutHandlers.FirstOrDefault(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase));
+
+ if (handler != null)
+ {
+ return handler.Resolve(filename);
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Creates the shortcut.
+ /// </summary>
+ /// <param name="shortcutPath">The shortcut path.</param>
+ /// <param name="target">The target.</param>
+ /// <exception cref="System.ArgumentNullException">
+ /// shortcutPath
+ /// or
+ /// target
+ /// </exception>
+ public void CreateShortcut(string shortcutPath, string target)
+ {
+ if (string.IsNullOrEmpty(shortcutPath))
+ {
+ throw new ArgumentNullException("shortcutPath");
+ }
+
+ if (string.IsNullOrEmpty(target))
+ {
+ throw new ArgumentNullException("target");
+ }
+
+ var extension = Path.GetExtension(shortcutPath);
+ var handler = _shortcutHandlers.FirstOrDefault(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase));
+
+ if (handler != null)
+ {
+ handler.Create(shortcutPath, target);
+ }
+ else
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ /// <summary>
+ /// Returns a <see cref="FileSystemMetadata"/> object for the specified file or directory path.
+ /// </summary>
+ /// <param name="path">A path to a file or directory.</param>
+ /// <returns>A <see cref="FileSystemMetadata"/> object.</returns>
+ /// <remarks>If the specified path points to a directory, the returned <see cref="FileSystemMetadata"/> object's
+ /// <see cref="FileSystemMetadata.IsDirectory"/> property will be set to true and all other properties will reflect the properties of the directory.</remarks>
+ public FileSystemMetadata GetFileSystemInfo(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentNullException("path");
+ }
+
+ // Take a guess to try and avoid two file system hits, but we'll double-check by calling Exists
+ if (Path.HasExtension(path))
+ {
+ var fileInfo = new FileInfo(path);
+
+ if (fileInfo.Exists)
+ {
+ return GetFileSystemMetadata(fileInfo);
+ }
+
+ return GetFileSystemMetadata(new DirectoryInfo(path));
+ }
+ else
+ {
+ var fileInfo = new DirectoryInfo(path);
+
+ if (fileInfo.Exists)
+ {
+ return GetFileSystemMetadata(fileInfo);
+ }
+
+ return GetFileSystemMetadata(new FileInfo(path));
+ }
+ }
+
+ /// <summary>
+ /// Returns a <see cref="FileSystemMetadata"/> object for the specified file path.
+ /// </summary>
+ /// <param name="path">A path to a file.</param>
+ /// <returns>A <see cref="FileSystemMetadata"/> object.</returns>
+ /// <remarks><para>If the specified path points to a directory, the returned <see cref="FileSystemMetadata"/> object's
+ /// <see cref="FileSystemMetadata.IsDirectory"/> property and the <see cref="FileSystemMetadata.Exists"/> property will both be set to false.</para>
+ /// <para>For automatic handling of files <b>and</b> directories, use <see cref="GetFileSystemInfo"/>.</para></remarks>
+ public FileSystemMetadata GetFileInfo(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentNullException("path");
+ }
+
+ var fileInfo = new FileInfo(path);
+
+ return GetFileSystemMetadata(fileInfo);
+ }
+
+ /// <summary>
+ /// Returns a <see cref="FileSystemMetadata"/> object for the specified directory path.
+ /// </summary>
+ /// <param name="path">A path to a directory.</param>
+ /// <returns>A <see cref="FileSystemMetadata"/> object.</returns>
+ /// <remarks><para>If the specified path points to a file, the returned <see cref="FileSystemMetadata"/> object's
+ /// <see cref="FileSystemMetadata.IsDirectory"/> property will be set to true and the <see cref="FileSystemMetadata.Exists"/> property will be set to false.</para>
+ /// <para>For automatic handling of files <b>and</b> directories, use <see cref="GetFileSystemInfo"/>.</para></remarks>
+ public FileSystemMetadata GetDirectoryInfo(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentNullException("path");
+ }
+
+ var fileInfo = new DirectoryInfo(path);
+
+ return GetFileSystemMetadata(fileInfo);
+ }
+
+ private FileSystemMetadata GetFileSystemMetadata(FileSystemInfo info)
+ {
+ var result = new FileSystemMetadata();
+
+ result.Exists = info.Exists;
+ result.FullName = info.FullName;
+ result.Extension = info.Extension;
+ result.Name = info.Name;
+
+ if (result.Exists)
+ {
+ var attributes = info.Attributes;
+ result.IsDirectory = info is DirectoryInfo || (attributes & FileAttributes.Directory) == FileAttributes.Directory;
+ result.IsHidden = (attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
+ result.IsReadOnly = (attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly;
+
+ var fileInfo = info as FileInfo;
+ if (fileInfo != null)
+ {
+ result.Length = fileInfo.Length;
+ result.DirectoryName = fileInfo.DirectoryName;
+ }
+
+ result.CreationTimeUtc = GetCreationTimeUtc(info);
+ result.LastWriteTimeUtc = GetLastWriteTimeUtc(info);
+ }
+ else
+ {
+ result.IsDirectory = info is DirectoryInfo;
+ }
+
+ return result;
+ }
+
+ /// <summary>
+ /// The space char
+ /// </summary>
+ private const char SpaceChar = ' ';
+
+ /// <summary>
+ /// Takes a filename and removes invalid characters
+ /// </summary>
+ /// <param name="filename">The filename.</param>
+ /// <returns>System.String.</returns>
+ /// <exception cref="System.ArgumentNullException">filename</exception>
+ public string GetValidFilename(string filename)
+ {
+ if (string.IsNullOrEmpty(filename))
+ {
+ throw new ArgumentNullException("filename");
+ }
+
+ var builder = new StringBuilder(filename);
+
+ foreach (var c in _invalidFileNameChars)
+ {
+ builder = builder.Replace(c, SpaceChar);
+ }
+
+ return builder.ToString();
+ }
+
+ /// <summary>
+ /// Gets the creation time UTC.
+ /// </summary>
+ /// <param name="info">The info.</param>
+ /// <returns>DateTime.</returns>
+ public DateTime GetCreationTimeUtc(FileSystemInfo info)
+ {
+ // This could throw an error on some file systems that have dates out of range
+ try
+ {
+ return info.CreationTimeUtc;
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error determining CreationTimeUtc for {0}", ex, info.FullName);
+ return DateTime.MinValue;
+ }
+ }
+
+ /// <summary>
+ /// Gets the creation time UTC.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>DateTime.</returns>
+ public DateTime GetCreationTimeUtc(string path)
+ {
+ return GetCreationTimeUtc(GetFileSystemInfo(path));
+ }
+
+ public DateTime GetCreationTimeUtc(FileSystemMetadata info)
+ {
+ return info.CreationTimeUtc;
+ }
+
+ public DateTime GetLastWriteTimeUtc(FileSystemMetadata info)
+ {
+ return info.LastWriteTimeUtc;
+ }
+
+ /// <summary>
+ /// Gets the creation time UTC.
+ /// </summary>
+ /// <param name="info">The info.</param>
+ /// <returns>DateTime.</returns>
+ public DateTime GetLastWriteTimeUtc(FileSystemInfo info)
+ {
+ // This could throw an error on some file systems that have dates out of range
+ try
+ {
+ return info.LastWriteTimeUtc;
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error determining LastAccessTimeUtc for {0}", ex, info.FullName);
+ return DateTime.MinValue;
+ }
+ }
+
+ /// <summary>
+ /// Gets the last write time UTC.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>DateTime.</returns>
+ public DateTime GetLastWriteTimeUtc(string path)
+ {
+ return GetLastWriteTimeUtc(GetFileSystemInfo(path));
+ }
+
+ /// <summary>
+ /// Gets the file stream.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="mode">The mode.</param>
+ /// <param name="access">The access.</param>
+ /// <param name="share">The share.</param>
+ /// <param name="isAsync">if set to <c>true</c> [is asynchronous].</param>
+ /// <returns>FileStream.</returns>
+ public Stream GetFileStream(string path, FileOpenMode mode, FileAccessMode access, FileShareMode share, bool isAsync = false)
+ {
+ if (_supportsAsyncFileStreams && isAsync)
+ {
+ return new FileStream(path, GetFileMode(mode), GetFileAccess(access), GetFileShare(share), 262144, true);
+ }
+
+ return new FileStream(path, GetFileMode(mode), GetFileAccess(access), GetFileShare(share), 262144);
+ }
+
+ private FileMode GetFileMode(FileOpenMode mode)
+ {
+ switch (mode)
+ {
+ case FileOpenMode.Append:
+ return FileMode.Append;
+ case FileOpenMode.Create:
+ return FileMode.Create;
+ case FileOpenMode.CreateNew:
+ return FileMode.CreateNew;
+ case FileOpenMode.Open:
+ return FileMode.Open;
+ case FileOpenMode.OpenOrCreate:
+ return FileMode.OpenOrCreate;
+ case FileOpenMode.Truncate:
+ return FileMode.Truncate;
+ default:
+ throw new Exception("Unrecognized FileOpenMode");
+ }
+ }
+
+ private FileAccess GetFileAccess(FileAccessMode mode)
+ {
+ switch (mode)
+ {
+ case FileAccessMode.ReadWrite:
+ return FileAccess.ReadWrite;
+ case FileAccessMode.Write:
+ return FileAccess.Write;
+ case FileAccessMode.Read:
+ return FileAccess.Read;
+ default:
+ throw new Exception("Unrecognized FileAccessMode");
+ }
+ }
+
+ private FileShare GetFileShare(FileShareMode mode)
+ {
+ switch (mode)
+ {
+ case FileShareMode.ReadWrite:
+ return FileShare.ReadWrite;
+ case FileShareMode.Write:
+ return FileShare.Write;
+ case FileShareMode.Read:
+ return FileShare.Read;
+ case FileShareMode.None:
+ return FileShare.None;
+ default:
+ throw new Exception("Unrecognized FileShareMode");
+ }
+ }
+
+ public void SetHidden(string path, bool isHidden)
+ {
+ var info = GetFileInfo(path);
+
+ if (info.Exists && info.IsHidden != isHidden)
+ {
+ if (isHidden)
+ {
+ File.SetAttributes(path, File.GetAttributes(path) | FileAttributes.Hidden);
+ }
+ else
+ {
+ FileAttributes attributes = File.GetAttributes(path);
+ attributes = RemoveAttribute(attributes, FileAttributes.Hidden);
+ File.SetAttributes(path, attributes);
+ }
+ }
+ }
+
+ public void SetReadOnly(string path, bool isReadOnly)
+ {
+ var info = GetFileInfo(path);
+
+ if (info.Exists && info.IsReadOnly != isReadOnly)
+ {
+ if (isReadOnly)
+ {
+ File.SetAttributes(path, File.GetAttributes(path) | FileAttributes.ReadOnly);
+ }
+ else
+ {
+ FileAttributes attributes = File.GetAttributes(path);
+ attributes = RemoveAttribute(attributes, FileAttributes.ReadOnly);
+ File.SetAttributes(path, attributes);
+ }
+ }
+ }
+
+ private static FileAttributes RemoveAttribute(FileAttributes attributes, FileAttributes attributesToRemove)
+ {
+ return attributes & ~attributesToRemove;
+ }
+
+ /// <summary>
+ /// Swaps the files.
+ /// </summary>
+ /// <param name="file1">The file1.</param>
+ /// <param name="file2">The file2.</param>
+ public void SwapFiles(string file1, string file2)
+ {
+ if (string.IsNullOrEmpty(file1))
+ {
+ throw new ArgumentNullException("file1");
+ }
+
+ if (string.IsNullOrEmpty(file2))
+ {
+ throw new ArgumentNullException("file2");
+ }
+
+ var temp1 = Path.GetTempFileName();
+
+ // Copying over will fail against hidden files
+ RemoveHiddenAttribute(file1);
+ RemoveHiddenAttribute(file2);
+
+ CopyFile(file1, temp1, true);
+
+ CopyFile(file2, file1, true);
+ CopyFile(temp1, file2, true);
+
+ DeleteFile(temp1);
+ }
+
+ /// <summary>
+ /// Removes the hidden attribute.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ private void RemoveHiddenAttribute(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentNullException("path");
+ }
+
+ var currentFile = new FileInfo(path);
+
+ // This will fail if the file is hidden
+ if (currentFile.Exists)
+ {
+ if ((currentFile.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden)
+ {
+ currentFile.Attributes &= ~FileAttributes.Hidden;
+ }
+ }
+ }
+
+ public bool ContainsSubPath(string parentPath, string path)
+ {
+ if (string.IsNullOrEmpty(parentPath))
+ {
+ throw new ArgumentNullException("parentPath");
+ }
+
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentNullException("path");
+ }
+
+ return path.IndexOf(parentPath.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) != -1;
+ }
+
+ public bool IsRootPath(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentNullException("path");
+ }
+
+ var parent = Path.GetDirectoryName(path);
+
+ if (!string.IsNullOrEmpty(parent))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ public string NormalizePath(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ throw new ArgumentNullException("path");
+ }
+
+ if (path.EndsWith(":\\", StringComparison.OrdinalIgnoreCase))
+ {
+ return path;
+ }
+
+ return path.TrimEnd(Path.DirectorySeparatorChar);
+ }
+
+ public string GetFileNameWithoutExtension(FileSystemMetadata info)
+ {
+ if (info.IsDirectory)
+ {
+ return info.Name;
+ }
+
+ return Path.GetFileNameWithoutExtension(info.FullName);
+ }
+
+ public string GetFileNameWithoutExtension(string path)
+ {
+ return Path.GetFileNameWithoutExtension(path);
+ }
+
+ public bool IsPathFile(string path)
+ {
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ throw new ArgumentNullException("path");
+ }
+
+ // Cannot use Path.IsPathRooted because it returns false under mono when using windows-based paths, e.g. C:\\
+
+ if (path.IndexOf("://", StringComparison.OrdinalIgnoreCase) != -1 &&
+ !path.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+ return true;
+
+ //return Path.IsPathRooted(path);
+ }
+
+ public void DeleteFile(string path)
+ {
+ var fileInfo = GetFileInfo(path);
+
+ if (fileInfo.Exists)
+ {
+ if (fileInfo.IsHidden)
+ {
+ SetHidden(path, false);
+ }
+ if (fileInfo.IsReadOnly)
+ {
+ SetReadOnly(path, false);
+ }
+ }
+
+ File.Delete(path);
+ }
+
+ public void DeleteDirectory(string path, bool recursive)
+ {
+ Directory.Delete(path, recursive);
+ }
+
+ public void CreateDirectory(string path)
+ {
+ Directory.CreateDirectory(path);
+ }
+
+ public List<FileSystemMetadata> GetDrives()
+ {
+ // Only include drives in the ready state or this method could end up being very slow, waiting for drives to timeout
+ return DriveInfo.GetDrives().Where(d => d.IsReady).Select(d => new FileSystemMetadata
+ {
+ Name = GetName(d),
+ FullName = d.RootDirectory.FullName,
+ IsDirectory = true
+
+ }).ToList();
+ }
+
+ private string GetName(DriveInfo drive)
+ {
+ return drive.Name;
+ }
+
+ public IEnumerable<FileSystemMetadata> GetDirectories(string path, bool recursive = false)
+ {
+ var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
+
+ return ToMetadata(path, new DirectoryInfo(path).EnumerateDirectories("*", searchOption));
+ }
+
+ public IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false)
+ {
+ var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
+
+ return ToMetadata(path, new DirectoryInfo(path).EnumerateFiles("*", searchOption));
+ }
+
+ public IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false)
+ {
+ var directoryInfo = new DirectoryInfo(path);
+ var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
+
+ if (EnableFileSystemRequestConcat)
+ {
+ return ToMetadata(path, directoryInfo.EnumerateDirectories("*", searchOption))
+ .Concat(ToMetadata(path, directoryInfo.EnumerateFiles("*", searchOption)));
+ }
+
+ return ToMetadata(path, directoryInfo.EnumerateFileSystemInfos("*", searchOption));
+ }
+
+ private IEnumerable<FileSystemMetadata> ToMetadata(string parentPath, IEnumerable<FileSystemInfo> infos)
+ {
+ return infos.Select(i =>
+ {
+ try
+ {
+ return GetFileSystemMetadata(i);
+ }
+ catch (PathTooLongException)
+ {
+ // Can't log using the FullName because it will throw the PathTooLongExceptiona again
+ //Logger.Warn("Path too long: {0}", i.FullName);
+ Logger.Warn("File or directory path too long. Parent folder: {0}", parentPath);
+ return null;
+ }
+
+ }).Where(i => i != null);
+ }
+
+ public string[] ReadAllLines(string path)
+ {
+ return File.ReadAllLines(path);
+ }
+
+ public void WriteAllLines(string path, IEnumerable<string> lines)
+ {
+ File.WriteAllLines(path, lines);
+ }
+
+ public Stream OpenRead(string path)
+ {
+ return File.OpenRead(path);
+ }
+
+ public void CopyFile(string source, string target, bool overwrite)
+ {
+ File.Copy(source, target, overwrite);
+ }
+
+ public void MoveFile(string source, string target)
+ {
+ File.Move(source, target);
+ }
+
+ public void MoveDirectory(string source, string target)
+ {
+ Directory.Move(source, target);
+ }
+
+ public bool DirectoryExists(string path)
+ {
+ return Directory.Exists(path);
+ }
+
+ public bool FileExists(string path)
+ {
+ return File.Exists(path);
+ }
+
+ public string ReadAllText(string path)
+ {
+ return File.ReadAllText(path);
+ }
+
+ public byte[] ReadAllBytes(string path)
+ {
+ return File.ReadAllBytes(path);
+ }
+
+ public void WriteAllText(string path, string text, Encoding encoding)
+ {
+ File.WriteAllText(path, text, encoding);
+ }
+
+ public void WriteAllText(string path, string text)
+ {
+ File.WriteAllText(path, text);
+ }
+
+ public void WriteAllBytes(string path, byte[] bytes)
+ {
+ File.WriteAllBytes(path, bytes);
+ }
+
+ public string ReadAllText(string path, Encoding encoding)
+ {
+ return File.ReadAllText(path, encoding);
+ }
+
+ public IEnumerable<string> GetDirectoryPaths(string path, bool recursive = false)
+ {
+ var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
+ return Directory.EnumerateDirectories(path, "*", searchOption);
+ }
+
+ public IEnumerable<string> GetFilePaths(string path, bool recursive = false)
+ {
+ var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
+ return Directory.EnumerateFiles(path, "*", searchOption);
+ }
+
+ public IEnumerable<string> GetFileSystemEntryPaths(string path, bool recursive = false)
+ {
+ var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
+ return Directory.EnumerateFileSystemEntries(path, "*", searchOption);
+ }
+
+ public virtual void SetExecutable(string path)
+ {
+
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/Logging/NLogger.cs b/Emby.Common.Implementations/Logging/NLogger.cs
new file mode 100644
index 0000000000..8abd3d0d92
--- /dev/null
+++ b/Emby.Common.Implementations/Logging/NLogger.cs
@@ -0,0 +1,224 @@
+using MediaBrowser.Model.Logging;
+using System;
+using System.Text;
+
+namespace Emby.Common.Implementations.Logging
+{
+ /// <summary>
+ /// Class NLogger
+ /// </summary>
+ public class NLogger : ILogger
+ {
+ /// <summary>
+ /// The _logger
+ /// </summary>
+ private readonly NLog.Logger _logger;
+
+ private readonly ILogManager _logManager;
+
+ /// <summary>
+ /// The _lock object
+ /// </summary>
+ private static readonly object LockObject = new object();
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NLogger" /> class.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <param name="logManager">The log manager.</param>
+ public NLogger(string name, ILogManager logManager)
+ {
+ _logManager = logManager;
+ lock (LockObject)
+ {
+ _logger = NLog.LogManager.GetLogger(name);
+ }
+ }
+
+ /// <summary>
+ /// Infoes the specified message.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <param name="paramList">The param list.</param>
+ public void Info(string message, params object[] paramList)
+ {
+ _logger.Info(message, paramList);
+ }
+
+ /// <summary>
+ /// Errors the specified message.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <param name="paramList">The param list.</param>
+ public void Error(string message, params object[] paramList)
+ {
+ _logger.Error(message, paramList);
+ }
+
+ /// <summary>
+ /// Warns the specified message.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <param name="paramList">The param list.</param>
+ public void Warn(string message, params object[] paramList)
+ {
+ _logger.Warn(message, paramList);
+ }
+
+ /// <summary>
+ /// Debugs the specified message.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <param name="paramList">The param list.</param>
+ public void Debug(string message, params object[] paramList)
+ {
+ if (_logManager.LogSeverity == LogSeverity.Info)
+ {
+ return;
+ }
+
+ _logger.Debug(message, paramList);
+ }
+
+ /// <summary>
+ /// Logs the exception.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <param name="exception">The exception.</param>
+ /// <param name="paramList">The param list.</param>
+ /// <exception cref="System.NotImplementedException"></exception>
+ public void ErrorException(string message, Exception exception, params object[] paramList)
+ {
+ LogException(LogSeverity.Error, message, exception, paramList);
+ }
+
+ /// <summary>
+ /// Logs the exception.
+ /// </summary>
+ /// <param name="level">The level.</param>
+ /// <param name="message">The message.</param>
+ /// <param name="exception">The exception.</param>
+ /// <param name="paramList">The param list.</param>
+ private void LogException(LogSeverity level, string message, Exception exception, params object[] paramList)
+ {
+ message = FormatMessage(message, paramList).Replace(Environment.NewLine, ". ");
+
+ var messageText = LogHelper.GetLogMessage(exception);
+
+ var prefix = _logManager.ExceptionMessagePrefix;
+
+ if (!string.IsNullOrWhiteSpace(prefix))
+ {
+ messageText.Insert(0, prefix);
+ }
+
+ LogMultiline(message, level, messageText);
+ }
+
+ /// <summary>
+ /// Formats the message.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <param name="paramList">The param list.</param>
+ /// <returns>System.String.</returns>
+ private static string FormatMessage(string message, params object[] paramList)
+ {
+ if (paramList != null)
+ {
+ for (var i = 0; i < paramList.Length; i++)
+ {
+ var obj = paramList[i];
+
+ message = message.Replace("{" + i + "}", (obj == null ? "null" : obj.ToString()));
+ }
+ }
+
+ return message;
+ }
+
+ /// <summary>
+ /// Logs the multiline.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <param name="severity">The severity.</param>
+ /// <param name="additionalContent">Content of the additional.</param>
+ public void LogMultiline(string message, LogSeverity severity, StringBuilder additionalContent)
+ {
+ if (severity == LogSeverity.Debug && _logManager.LogSeverity == LogSeverity.Info)
+ {
+ return;
+ }
+
+ additionalContent.Insert(0, message + Environment.NewLine);
+
+ const char tabChar = '\t';
+
+ var text = additionalContent.ToString()
+ .Replace(Environment.NewLine, Environment.NewLine + tabChar)
+ .TrimEnd(tabChar);
+
+ if (text.EndsWith(Environment.NewLine))
+ {
+ text = text.Substring(0, text.LastIndexOf(Environment.NewLine, StringComparison.OrdinalIgnoreCase));
+ }
+
+ _logger.Log(GetLogLevel(severity), text);
+ }
+
+ /// <summary>
+ /// Gets the log level.
+ /// </summary>
+ /// <param name="severity">The severity.</param>
+ /// <returns>NLog.LogLevel.</returns>
+ private NLog.LogLevel GetLogLevel(LogSeverity severity)
+ {
+ switch (severity)
+ {
+ case LogSeverity.Debug:
+ return NLog.LogLevel.Debug;
+ case LogSeverity.Error:
+ return NLog.LogLevel.Error;
+ case LogSeverity.Warn:
+ return NLog.LogLevel.Warn;
+ case LogSeverity.Fatal:
+ return NLog.LogLevel.Fatal;
+ case LogSeverity.Info:
+ return NLog.LogLevel.Info;
+ default:
+ throw new ArgumentException("Unknown LogSeverity: " + severity.ToString());
+ }
+ }
+
+ /// <summary>
+ /// Logs the specified severity.
+ /// </summary>
+ /// <param name="severity">The severity.</param>
+ /// <param name="message">The message.</param>
+ /// <param name="paramList">The param list.</param>
+ public void Log(LogSeverity severity, string message, params object[] paramList)
+ {
+ _logger.Log(GetLogLevel(severity), message, paramList);
+ }
+
+ /// <summary>
+ /// Fatals the specified message.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <param name="paramList">The param list.</param>
+ public void Fatal(string message, params object[] paramList)
+ {
+ _logger.Fatal(message, paramList);
+ }
+
+ /// <summary>
+ /// Fatals the exception.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <param name="exception">The exception.</param>
+ /// <param name="paramList">The param list.</param>
+ public void FatalException(string message, Exception exception, params object[] paramList)
+ {
+ LogException(LogSeverity.Fatal, message, exception, paramList);
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/Logging/NlogManager.cs b/Emby.Common.Implementations/Logging/NlogManager.cs
new file mode 100644
index 0000000000..f7b723e8bc
--- /dev/null
+++ b/Emby.Common.Implementations/Logging/NlogManager.cs
@@ -0,0 +1,544 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Xml;
+using NLog;
+using NLog.Config;
+using NLog.Filters;
+using NLog.Targets;
+using NLog.Targets.Wrappers;
+using MediaBrowser.Model.Logging;
+
+namespace Emby.Common.Implementations.Logging
+{
+ /// <summary>
+ /// Class NlogManager
+ /// </summary>
+ public class NlogManager : ILogManager
+ {
+ #region Private Fields
+
+ private LogSeverity _severity = LogSeverity.Debug;
+
+ /// <summary>
+ /// Gets or sets the log directory.
+ /// </summary>
+ /// <value>The log directory.</value>
+ private readonly string LogDirectory;
+
+ /// <summary>
+ /// Gets or sets the log file prefix.
+ /// </summary>
+ /// <value>The log file prefix.</value>
+ private readonly string LogFilePrefix;
+
+ #endregion
+
+ #region Event Declarations
+
+ /// <summary>
+ /// Occurs when [logger loaded].
+ /// </summary>
+ public event EventHandler LoggerLoaded;
+
+ #endregion
+
+ #region Public Properties
+
+ /// <summary>
+ /// Gets the log file path.
+ /// </summary>
+ /// <value>The log file path.</value>
+ public string LogFilePath { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the exception message prefix.
+ /// </summary>
+ /// <value>The exception message prefix.</value>
+ public string ExceptionMessagePrefix { get; set; }
+
+ public string NLogConfigurationFilePath { get; set; }
+
+ public LogSeverity LogSeverity
+ {
+
+ get
+ {
+ return _severity;
+ }
+
+ set
+ {
+ DebugFileWriter(
+ LogDirectory, String.Format(
+ "SET LogSeverity, _severity = [{0}], value = [{1}]",
+ _severity.ToString(),
+ value.ToString()
+ ));
+
+ var changed = _severity != value;
+
+ _severity = value;
+
+ if (changed)
+ {
+ UpdateLogLevel(value);
+ }
+
+ }
+ }
+
+ #endregion
+
+ #region Constructor(s)
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NlogManager" /> class.
+ /// </summary>
+ /// <param name="logDirectory">The log directory.</param>
+ /// <param name="logFileNamePrefix">The log file name prefix.</param>
+ public NlogManager(string logDirectory, string logFileNamePrefix)
+ {
+ DebugFileWriter(
+ logDirectory, String.Format(
+ "NlogManager constructor called, logDirectory is [{0}], logFileNamePrefix is [{1}], _severity is [{2}].",
+ logDirectory,
+ logFileNamePrefix,
+ _severity.ToString()
+ ));
+
+ LogDirectory = logDirectory;
+ LogFilePrefix = logFileNamePrefix;
+
+ LogManager.Configuration = new LoggingConfiguration();
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NlogManager" /> class.
+ /// </summary>
+ /// <param name="logDirectory">The log directory.</param>
+ /// <param name="logFileNamePrefix">The log file name prefix.</param>
+ public NlogManager(string logDirectory, string logFileNamePrefix, LogSeverity initialSeverity) : this(logDirectory, logFileNamePrefix)
+ {
+ _severity = initialSeverity;
+
+ DebugFileWriter(
+ logDirectory, String.Format(
+ "NlogManager constructor called, logDirectory is [{0}], logFileNamePrefix is [{1}], _severity is [{2}].",
+ logDirectory,
+ logFileNamePrefix,
+ _severity.ToString()
+ ));
+ }
+
+ #endregion
+
+ #region Private Methods
+
+ /// <summary>
+ /// Adds the file target.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="level">The level.</param>
+ private void AddFileTarget(string path, LogSeverity level)
+ {
+
+ DebugFileWriter(
+ LogDirectory, String.Format(
+ "AddFileTarget called, path = [{0}], level = [{1}].",
+ path,
+ level.ToString()
+ ));
+
+ RemoveTarget("ApplicationLogFileWrapper");
+
+ var wrapper = new AsyncTargetWrapper();
+ wrapper.Name = "ApplicationLogFileWrapper";
+
+ var logFile = new FileTarget
+ {
+ FileName = path,
+ Layout = "${longdate} ${level} ${logger}: ${message}"
+ };
+
+ logFile.Name = "ApplicationLogFile";
+
+ wrapper.WrappedTarget = logFile;
+
+ AddLogTarget(wrapper, level);
+
+ }
+
+ /// <summary>
+ /// Gets the log level.
+ /// </summary>
+ /// <param name="severity">The severity.</param>
+ /// <returns>LogLevel.</returns>
+ /// <exception cref="System.ArgumentException">Unrecognized LogSeverity</exception>
+ private LogLevel GetLogLevel(LogSeverity severity)
+ {
+ switch (severity)
+ {
+ case LogSeverity.Debug:
+ return LogLevel.Debug;
+ case LogSeverity.Error:
+ return LogLevel.Error;
+ case LogSeverity.Fatal:
+ return LogLevel.Fatal;
+ case LogSeverity.Info:
+ return LogLevel.Info;
+ case LogSeverity.Warn:
+ return LogLevel.Warn;
+ default:
+ throw new ArgumentException("Unrecognized LogSeverity");
+ }
+ }
+
+ private void UpdateLogLevel(LogSeverity newLevel)
+ {
+ DebugFileWriter(
+ LogDirectory, String.Format(
+ "UpdateLogLevel called, newLevel = [{0}].",
+ newLevel.ToString()
+ ));
+
+ var level = GetLogLevel(newLevel);
+
+ var rules = LogManager.Configuration.LoggingRules;
+
+ foreach (var rule in rules)
+ {
+ if (!rule.IsLoggingEnabledForLevel(level))
+ {
+ rule.EnableLoggingForLevel(level);
+ }
+ foreach (var lev in rule.Levels.ToArray())
+ {
+ if (lev < level)
+ {
+ rule.DisableLoggingForLevel(lev);
+ }
+ }
+ }
+ }
+
+ private void AddCustomFilters(string defaultLoggerNamePattern, LoggingRule defaultRule)
+ {
+ DebugFileWriter(
+ LogDirectory, String.Format(
+ "AddCustomFilters called, defaultLoggerNamePattern = [{0}], defaultRule.LoggerNamePattern = [{1}].",
+ defaultLoggerNamePattern,
+ defaultRule.LoggerNamePattern
+ ));
+
+ try
+ {
+ var customConfig = new NLog.Config.XmlLoggingConfiguration(NLogConfigurationFilePath);
+
+ DebugFileWriter(
+ LogDirectory, String.Format(
+ "Custom Configuration Loaded, Rule Count = [{0}].",
+ customConfig.LoggingRules.Count.ToString()
+ ));
+
+ foreach (var customRule in customConfig.LoggingRules)
+ {
+
+ DebugFileWriter(
+ LogDirectory, String.Format(
+ "Read Custom Rule, LoggerNamePattern = [{0}], Targets = [{1}].",
+ customRule.LoggerNamePattern,
+ string.Join(",", customRule.Targets.Select(x => x.Name).ToList())
+ ));
+
+ if (customRule.LoggerNamePattern.Equals(defaultLoggerNamePattern))
+ {
+
+ if (customRule.Targets.Any((arg) => arg.Name.Equals(defaultRule.Targets.First().Name)))
+ {
+
+ DebugFileWriter(
+ LogDirectory, String.Format(
+ "Custom rule filters can be applied to this target, Filter Count = [{0}].",
+ customRule.Filters.Count.ToString()
+ ));
+
+ foreach (ConditionBasedFilter customFilter in customRule.Filters)
+ {
+
+ DebugFileWriter(
+ LogDirectory, String.Format(
+ "Read Custom Filter, Filter = [{0}], Action = [{1}], Type = [{2}].",
+ customFilter.Condition.ToString(),
+ customFilter.Action.ToString(),
+ customFilter.GetType().ToString()
+ ));
+
+ defaultRule.Filters.Add(customFilter);
+
+ }
+ }
+ else
+ {
+
+ DebugFileWriter(
+ LogDirectory, String.Format(
+ "Ignoring custom rule as [Target] does not match."
+ ));
+
+ }
+
+ }
+ else
+ {
+
+ DebugFileWriter(
+ LogDirectory, String.Format(
+ "Ignoring custom rule as [LoggerNamePattern] does not match."
+ ));
+
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ // Intentionally do nothing, prevent issues affecting normal execution.
+ DebugFileWriter(
+ LogDirectory, String.Format(
+ "Exception in AddCustomFilters, ex.Message = [{0}].",
+ ex.Message
+ )
+ );
+
+ }
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ /// <summary>
+ /// Gets the logger.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>ILogger.</returns>
+ public MediaBrowser.Model.Logging.ILogger GetLogger(string name)
+ {
+
+ DebugFileWriter(
+ LogDirectory, String.Format(
+ "GetLogger called, name = [{0}].",
+ name
+ ));
+
+ return new NLogger(name, this);
+
+ }
+
+ /// <summary>
+ /// Adds the log target.
+ /// </summary>
+ /// <param name="target">The target.</param>
+ /// <param name="level">The level.</param>
+ public void AddLogTarget(Target target, LogSeverity level)
+ {
+
+ DebugFileWriter(
+ LogDirectory, String.Format(
+ "AddLogTarget called, target.Name = [{0}], level = [{1}].",
+ target.Name,
+ level.ToString()
+ ));
+
+ string loggerNamePattern = "*";
+ var config = LogManager.Configuration;
+ var rule = new LoggingRule(loggerNamePattern, GetLogLevel(level), target);
+
+ config.AddTarget(target.Name, target);
+
+ AddCustomFilters(loggerNamePattern, rule);
+
+ config.LoggingRules.Add(rule);
+
+ LogManager.Configuration = config;
+
+ }
+
+ /// <summary>
+ /// Removes the target.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ public void RemoveTarget(string name)
+ {
+
+ DebugFileWriter(
+ LogDirectory, String.Format(
+ "RemoveTarget called, name = [{0}].",
+ name
+ ));
+
+ var config = LogManager.Configuration;
+
+ var target = config.FindTargetByName(name);
+
+ if (target != null)
+ {
+ foreach (var rule in config.LoggingRules.ToList())
+ {
+ var contains = rule.Targets.Contains(target);
+
+ rule.Targets.Remove(target);
+
+ if (contains)
+ {
+ config.LoggingRules.Remove(rule);
+ }
+ }
+
+ config.RemoveTarget(name);
+ LogManager.Configuration = config;
+ }
+ }
+
+ public void AddConsoleOutput()
+ {
+
+ DebugFileWriter(
+ LogDirectory, String.Format(
+ "AddConsoleOutput called."
+ ));
+
+ RemoveTarget("ConsoleTargetWrapper");
+
+ var wrapper = new AsyncTargetWrapper();
+ wrapper.Name = "ConsoleTargetWrapper";
+
+ var target = new ConsoleTarget()
+ {
+ Layout = "${level}, ${logger}, ${message}",
+ Error = false
+ };
+
+ target.Name = "ConsoleTarget";
+
+ wrapper.WrappedTarget = target;
+
+ AddLogTarget(wrapper, LogSeverity);
+
+ }
+
+ public void RemoveConsoleOutput()
+ {
+
+ DebugFileWriter(
+ LogDirectory, String.Format(
+ "RemoveConsoleOutput called."
+ ));
+
+ RemoveTarget("ConsoleTargetWrapper");
+
+ }
+
+ /// <summary>
+ /// Reloads the logger, maintaining the current log level.
+ /// </summary>
+ public void ReloadLogger()
+ {
+ ReloadLogger(LogSeverity);
+ }
+
+ /// <summary>
+ /// Reloads the logger, using the specified logging level.
+ /// </summary>
+ /// <param name="level">The level.</param>
+ public void ReloadLogger(LogSeverity level)
+ {
+
+ DebugFileWriter(
+ LogDirectory, String.Format(
+ "ReloadLogger called, level = [{0}], LogFilePath (existing) = [{1}].",
+ level.ToString(),
+ LogFilePath
+ ));
+
+ LogFilePath = Path.Combine(LogDirectory, LogFilePrefix + "-" + decimal.Floor(DateTime.Now.Ticks / 10000000) + ".txt");
+
+ Directory.CreateDirectory(Path.GetDirectoryName(LogFilePath));
+
+ AddFileTarget(LogFilePath, level);
+
+ LogSeverity = level;
+
+ if (LoggerLoaded != null)
+ {
+ try
+ {
+
+ DebugFileWriter(
+ LogDirectory, String.Format(
+ "ReloadLogger called, raised event LoggerLoaded."
+ ));
+
+ LoggerLoaded(this, EventArgs.Empty);
+
+ }
+ catch (Exception ex)
+ {
+ GetLogger("Logger").ErrorException("Error in LoggerLoaded event", ex);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Flushes this instance.
+ /// </summary>
+ public void Flush()
+ {
+
+ DebugFileWriter(
+ LogDirectory, String.Format(
+ "Flush called."
+ ));
+
+ LogManager.Flush();
+
+ }
+
+ #endregion
+
+ #region Conditional Debug Methods
+
+ /// <summary>
+ /// DEBUG: Standalone method to write out debug to assist with logger development/troubleshooting.
+ /// <list type="bullet">
+ /// <item><description>The output file will be written to the server's log directory.</description></item>
+ /// <item><description>Calls to the method are safe and will never throw any exceptions.</description></item>
+ /// <item><description>Method calls will be omitted unless the library is compiled with DEBUG defined.</description></item>
+ /// </list>
+ /// </summary>
+ private static void DebugFileWriter(string logDirectory, string message)
+ {
+#if DEBUG
+ try
+ {
+
+ System.IO.File.AppendAllText(
+ Path.Combine(logDirectory, "NlogManager.txt"),
+ String.Format(
+ "{0} : {1}{2}",
+ System.DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"),
+ message,
+ System.Environment.NewLine
+ )
+ );
+
+ }
+ catch (Exception ex)
+ {
+ // Intentionally do nothing, prevent issues affecting normal execution.
+ }
+#endif
+ }
+ #endregion
+ }
+} \ No newline at end of file
diff --git a/Emby.Common.Implementations/Net/DisposableManagedObjectBase.cs b/Emby.Common.Implementations/Net/DisposableManagedObjectBase.cs
new file mode 100644
index 0000000000..8476cea326
--- /dev/null
+++ b/Emby.Common.Implementations/Net/DisposableManagedObjectBase.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Emby.Common.Implementations.Net
+{
+ /// <summary>
+ /// Correclty implements the <see cref="IDisposable"/> interface and pattern for an object containing only managed resources, and adds a few common niceities not on the interface such as an <see cref="IsDisposed"/> property.
+ /// </summary>
+ public abstract class DisposableManagedObjectBase : IDisposable
+ {
+
+ #region Public Methods
+
+ /// <summary>
+ /// Override this method and dispose any objects you own the lifetime of if disposing is true;
+ /// </summary>
+ /// <param name="disposing">True if managed objects should be disposed, if false, only unmanaged resources should be released.</param>
+ protected abstract void Dispose(bool disposing);
+
+ /// <summary>
+ /// Throws and <see cref="System.ObjectDisposedException"/> if the <see cref="IsDisposed"/> property is true.
+ /// </summary>
+ /// <seealso cref="IsDisposed"/>
+ /// <exception cref="System.ObjectDisposedException">Thrown if the <see cref="IsDisposed"/> property is true.</exception>
+ /// <seealso cref="Dispose()"/>
+ protected virtual void ThrowIfDisposed()
+ {
+ if (this.IsDisposed) throw new ObjectDisposedException(this.GetType().FullName);
+ }
+
+ #endregion
+
+ #region Public Properties
+
+ /// <summary>
+ /// Sets or returns a boolean indicating whether or not this instance has been disposed.
+ /// </summary>
+ /// <seealso cref="Dispose()"/>
+ public bool IsDisposed
+ {
+ get;
+ private set;
+ }
+
+ #endregion
+
+ #region IDisposable Members
+
+ /// <summary>
+ /// Disposes this object instance and all internally managed resources.
+ /// </summary>
+ /// <remarks>
+ /// <para>Sets the <see cref="IsDisposed"/> property to true. Does not explicitly throw an exception if called multiple times, but makes no promises about behaviour of derived classes.</para>
+ /// </remarks>
+ /// <seealso cref="IsDisposed"/>
+ public void Dispose()
+ {
+ try
+ {
+ IsDisposed = true;
+
+ Dispose(true);
+ }
+ finally
+ {
+ GC.SuppressFinalize(this);
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/Emby.Common.Implementations/Net/NetSocket.cs b/Emby.Common.Implementations/Net/NetSocket.cs
new file mode 100644
index 0000000000..bc012dfe2d
--- /dev/null
+++ b/Emby.Common.Implementations/Net/NetSocket.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading;
+using Emby.Common.Implementations.Networking;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Logging;
+
+namespace Emby.Common.Implementations.Net
+{
+ public class NetSocket : ISocket
+ {
+ public Socket Socket { get; private set; }
+ private readonly ILogger _logger;
+
+ public bool DualMode { get; private set; }
+
+ public NetSocket(Socket socket, ILogger logger, bool isDualMode)
+ {
+ if (socket == null)
+ {
+ throw new ArgumentNullException("socket");
+ }
+ if (logger == null)
+ {
+ throw new ArgumentNullException("logger");
+ }
+
+ Socket = socket;
+ _logger = logger;
+ DualMode = isDualMode;
+ }
+
+ public IpEndPointInfo LocalEndPoint
+ {
+ get
+ {
+ return NetworkManager.ToIpEndPointInfo((IPEndPoint)Socket.LocalEndPoint);
+ }
+ }
+
+ public IpEndPointInfo RemoteEndPoint
+ {
+ get
+ {
+ return NetworkManager.ToIpEndPointInfo((IPEndPoint)Socket.RemoteEndPoint);
+ }
+ }
+
+ public void Close()
+ {
+#if NET46
+ Socket.Close();
+#else
+ Socket.Dispose();
+#endif
+ }
+
+ public void Shutdown(bool both)
+ {
+ if (both)
+ {
+ Socket.Shutdown(SocketShutdown.Both);
+ }
+ else
+ {
+ // Change interface if ever needed
+ throw new NotImplementedException();
+ }
+ }
+
+ public void Listen(int backlog)
+ {
+ Socket.Listen(backlog);
+ }
+
+ public void Bind(IpEndPointInfo endpoint)
+ {
+ var nativeEndpoint = NetworkManager.ToIPEndPoint(endpoint);
+
+ Socket.Bind(nativeEndpoint);
+ }
+
+ private SocketAcceptor _acceptor;
+ public void StartAccept(Action<ISocket> onAccept, Func<bool> isClosed)
+ {
+ _acceptor = new SocketAcceptor(_logger, Socket, onAccept, isClosed, DualMode);
+
+ _acceptor.StartAccept();
+ }
+
+ public void Dispose()
+ {
+ Socket.Dispose();
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/Net/SocketAcceptor.cs b/Emby.Common.Implementations/Net/SocketAcceptor.cs
new file mode 100644
index 0000000000..d4c6d33e52
--- /dev/null
+++ b/Emby.Common.Implementations/Net/SocketAcceptor.cs
@@ -0,0 +1,127 @@
+using System;
+using System.Net.Sockets;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Net;
+
+namespace Emby.Common.Implementations.Net
+{
+ public class SocketAcceptor
+ {
+ private readonly ILogger _logger;
+ private readonly Socket _originalSocket;
+ private readonly Func<bool> _isClosed;
+ private readonly Action<ISocket> _onAccept;
+ private readonly bool _isDualMode;
+
+ public SocketAcceptor(ILogger logger, Socket originalSocket, Action<ISocket> onAccept, Func<bool> isClosed, bool isDualMode)
+ {
+ if (logger == null)
+ {
+ throw new ArgumentNullException("logger");
+ }
+ if (originalSocket == null)
+ {
+ throw new ArgumentNullException("originalSocket");
+ }
+ if (onAccept == null)
+ {
+ throw new ArgumentNullException("onAccept");
+ }
+ if (isClosed == null)
+ {
+ throw new ArgumentNullException("isClosed");
+ }
+
+ _logger = logger;
+ _originalSocket = originalSocket;
+ _isClosed = isClosed;
+ _isDualMode = isDualMode;
+ _onAccept = onAccept;
+ }
+
+ public void StartAccept()
+ {
+ Socket dummy = null;
+ StartAccept(null, ref dummy);
+ }
+
+ public void StartAccept(SocketAsyncEventArgs acceptEventArg, ref Socket accepted)
+ {
+ if (acceptEventArg == null)
+ {
+ acceptEventArg = new SocketAsyncEventArgs();
+ acceptEventArg.Completed += new EventHandler<SocketAsyncEventArgs>(AcceptEventArg_Completed);
+ }
+ else
+ {
+ // socket must be cleared since the context object is being reused
+ acceptEventArg.AcceptSocket = null;
+ }
+
+ try
+ {
+ bool willRaiseEvent = _originalSocket.AcceptAsync(acceptEventArg);
+
+ if (!willRaiseEvent)
+ {
+ ProcessAccept(acceptEventArg);
+ }
+ }
+ catch (Exception ex)
+ {
+ if (accepted != null)
+ {
+ try
+ {
+#if NET46
+ accepted.Close();
+#else
+ accepted.Dispose();
+#endif
+ }
+ catch
+ {
+ }
+ accepted = null;
+ }
+ }
+ }
+
+ // This method is the callback method associated with Socket.AcceptAsync
+ // operations and is invoked when an accept operation is complete
+ //
+ void AcceptEventArg_Completed(object sender, SocketAsyncEventArgs e)
+ {
+ ProcessAccept(e);
+ }
+
+ private void ProcessAccept(SocketAsyncEventArgs e)
+ {
+ if (_isClosed())
+ {
+ return;
+ }
+
+ // http://msdn.microsoft.com/en-us/library/system.net.sockets.socket.acceptasync%28v=vs.110%29.aspx
+ // Under certain conditions ConnectionReset can occur
+ // Need to attept to re-accept
+ if (e.SocketError == SocketError.ConnectionReset)
+ {
+ _logger.Error("SocketError.ConnectionReset reported. Attempting to re-accept.");
+ Socket dummy = null;
+ StartAccept(e, ref dummy);
+ return;
+ }
+
+ var acceptSocket = e.AcceptSocket;
+ if (acceptSocket != null)
+ {
+ //ProcessAccept(acceptSocket);
+ _onAccept(new NetSocket(acceptSocket, _logger, _isDualMode));
+ }
+
+ // Accept the next connection request
+ StartAccept(e, ref acceptSocket);
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/Net/SocketFactory.cs b/Emby.Common.Implementations/Net/SocketFactory.cs
new file mode 100644
index 0000000000..70c7ba8458
--- /dev/null
+++ b/Emby.Common.Implementations/Net/SocketFactory.cs
@@ -0,0 +1,160 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading.Tasks;
+using Emby.Common.Implementations.Networking;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Net;
+
+namespace Emby.Common.Implementations.Net
+{
+ public class SocketFactory : ISocketFactory
+ {
+ // 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.
+
+ private readonly ILogger _logger;
+
+ public SocketFactory(ILogger logger)
+ {
+ if (logger == null)
+ {
+ throw new ArgumentNullException("logger");
+ }
+
+ _logger = logger;
+ }
+
+ public ISocket CreateSocket(IpAddressFamily family, MediaBrowser.Model.Net.SocketType socketType, MediaBrowser.Model.Net.ProtocolType protocolType, bool dualMode)
+ {
+ try
+ {
+ var addressFamily = family == IpAddressFamily.InterNetwork
+ ? AddressFamily.InterNetwork
+ : AddressFamily.InterNetworkV6;
+
+ var socket = new Socket(addressFamily, System.Net.Sockets.SocketType.Stream, System.Net.Sockets.ProtocolType.Tcp);
+
+ if (dualMode)
+ {
+ socket.DualMode = true;
+ }
+
+ return new NetSocket(socket, _logger, dualMode);
+ }
+ catch (SocketException ex)
+ {
+ throw new SocketCreateException(ex.SocketErrorCode.ToString(), ex);
+ }
+ }
+
+ #region ISocketFactory Members
+
+ /// <summary>
+ /// Creates a new UDP socket and binds it to the specified local port.
+ /// </summary>
+ /// <param name="localPort">An integer specifying the local port to bind the socket to.</param>
+ public IUdpSocket CreateUdpSocket(int localPort)
+ {
+ if (localPort < 0) throw new ArgumentException("localPort cannot be less than zero.", "localPort");
+
+ var retVal = new Socket(AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Dgram, System.Net.Sockets.ProtocolType.Udp);
+ try
+ {
+ retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
+ return new UdpSocket(retVal, localPort, IPAddress.Any);
+ }
+ catch
+ {
+ if (retVal != null)
+ retVal.Dispose();
+
+ throw;
+ }
+ }
+
+ /// <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>
+ /// <returns>An implementation of the <see cref="IUdpSocket"/> interface used by RSSDP components to perform socket operations.</returns>
+ public IUdpSocket CreateSsdpUdpSocket(IpAddressInfo localIpAddress, int localPort)
+ {
+ if (localPort < 0) throw new ArgumentException("localPort cannot be less than zero.", "localPort");
+
+ var retVal = new Socket(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, 4);
+
+ var localIp = NetworkManager.ToIPAddress(localIpAddress);
+
+ retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(IPAddress.Parse("239.255.255.250"), localIp));
+ return new UdpSocket(retVal, localPort, localIp);
+ }
+ 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>
+ 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, System.Net.Sockets.SocketType.Dgram, System.Net.Sockets.ProtocolType.Udp);
+
+ try
+ {
+#if NET46
+ retVal.ExclusiveAddressUse = false;
+#else
+ // 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;
+ }
+#endif
+ //retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true);
+ retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
+ retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive);
+
+ var localIp = IPAddress.Any;
+
+ retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(IPAddress.Parse(ipAddress), localIp));
+ retVal.MulticastLoopback = true;
+
+ return new UdpSocket(retVal, localPort, localIp);
+ }
+ catch
+ {
+ if (retVal != null)
+ retVal.Dispose();
+
+ throw;
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/Emby.Common.Implementations/Net/UdpSocket.cs b/Emby.Common.Implementations/Net/UdpSocket.cs
new file mode 100644
index 0000000000..b2af9d162e
--- /dev/null
+++ b/Emby.Common.Implementations/Net/UdpSocket.cs
@@ -0,0 +1,242 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.Sockets;
+using System.Security;
+using System.Threading.Tasks;
+using Emby.Common.Implementations.Networking;
+using MediaBrowser.Model.Net;
+
+namespace Emby.Common.Implementations.Net
+{
+ // 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 Socket _Socket;
+ private int _LocalPort;
+ #endregion
+
+ #region Constructors
+
+ public UdpSocket(Socket socket, int localPort, IPAddress ip)
+ {
+ if (socket == null) throw new ArgumentNullException("socket");
+
+ _Socket = socket;
+ _LocalPort = localPort;
+ LocalIPAddress = NetworkManager.ToIpAddressInfo(ip);
+
+ _Socket.Bind(new IPEndPoint(ip, _LocalPort));
+ }
+
+ #endregion
+
+ public IpAddressInfo LocalIPAddress
+ {
+ get;
+ private set;
+ }
+
+ #region IUdpSocket Members
+
+ public Task<SocketReceiveResult> ReceiveAsync()
+ {
+ ThrowIfDisposed();
+
+ var tcs = new TaskCompletionSource<SocketReceiveResult>();
+
+ EndPoint receivedFromEndPoint = new IPEndPoint(IPAddress.Any, 0);
+ var state = new AsyncReceiveState(_Socket, receivedFromEndPoint);
+ state.TaskCompletionSource = tcs;
+
+#if NETSTANDARD1_6
+ _Socket.ReceiveFromAsync(new ArraySegment<Byte>(state.Buffer),SocketFlags.None, state.RemoteEndPoint)
+ .ContinueWith((task, asyncState) =>
+ {
+ if (task.Status != TaskStatus.Faulted)
+ {
+ var receiveState = asyncState as AsyncReceiveState;
+ receiveState.RemoteEndPoint = task.Result.RemoteEndPoint;
+ ProcessResponse(receiveState, () => task.Result.ReceivedBytes, LocalIPAddress);
+ }
+ }, state);
+#else
+ _Socket.BeginReceiveFrom(state.Buffer, 0, state.Buffer.Length, SocketFlags.None, ref state.RemoteEndPoint, ProcessResponse, state);
+#endif
+
+ return tcs.Task;
+ }
+
+ public Task SendAsync(byte[] buffer, int size, IpEndPointInfo endPoint)
+ {
+ ThrowIfDisposed();
+
+ if (buffer == null) throw new ArgumentNullException("messageData");
+ if (endPoint == null) throw new ArgumentNullException("endPoint");
+
+ var ipEndPoint = NetworkManager.ToIPEndPoint(endPoint);
+
+#if NETSTANDARD1_6
+
+ if (size != buffer.Length)
+ {
+ byte[] copy = new byte[size];
+ Buffer.BlockCopy(buffer, 0, copy, 0, size);
+ buffer = copy;
+ }
+
+ _Socket.SendTo(buffer, ipEndPoint);
+ return Task.FromResult(true);
+#else
+ var taskSource = new TaskCompletionSource<bool>();
+
+ try
+ {
+ _Socket.BeginSendTo(buffer, 0, size, SocketFlags.None, ipEndPoint, result =>
+ {
+ try
+ {
+ _Socket.EndSend(result);
+ taskSource.TrySetResult(true);
+ }
+ catch (Exception ex)
+ {
+ taskSource.TrySetException(ex);
+ }
+
+ }, null);
+ }
+ catch (Exception ex)
+ {
+ taskSource.TrySetException(ex);
+ }
+
+ //_Socket.SendTo(messageData, new System.Net.IPEndPoint(IPAddress.Parse(RemoteEndPoint.IPAddress), RemoteEndPoint.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
+
+ private static void ProcessResponse(AsyncReceiveState state, Func<int> receiveData, IpAddressInfo localIpAddress)
+ {
+ try
+ {
+ var bytesRead = receiveData();
+
+ var ipEndPoint = state.RemoteEndPoint as IPEndPoint;
+ state.TaskCompletionSource.SetResult(
+ new SocketReceiveResult
+ {
+ Buffer = state.Buffer,
+ ReceivedBytes = bytesRead,
+ RemoteEndPoint = ToIpEndPointInfo(ipEndPoint),
+ LocalIPAddress = localIpAddress
+ }
+ );
+ }
+ 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);
+ }
+ }
+
+ private static IpEndPointInfo ToIpEndPointInfo(IPEndPoint endpoint)
+ {
+ if (endpoint == null)
+ {
+ return null;
+ }
+
+ return NetworkManager.ToIpEndPointInfo(endpoint);
+ }
+
+ private void ProcessResponse(IAsyncResult asyncResult)
+ {
+#if NET46
+ var state = asyncResult.AsyncState as AsyncReceiveState;
+ try
+ {
+ var bytesRead = state.Socket.EndReceiveFrom(asyncResult, ref state.RemoteEndPoint);
+
+ var ipEndPoint = state.RemoteEndPoint as IPEndPoint;
+ state.TaskCompletionSource.SetResult(
+ new SocketReceiveResult
+ {
+ Buffer = state.Buffer,
+ ReceivedBytes = bytesRead,
+ RemoteEndPoint = ToIpEndPointInfo(ipEndPoint),
+ LocalIPAddress = LocalIPAddress
+ }
+ );
+ }
+ catch (ObjectDisposedException)
+ {
+ state.TaskCompletionSource.SetCanceled();
+ }
+ catch (Exception ex)
+ {
+ state.TaskCompletionSource.SetException(ex);
+ }
+#endif
+ }
+
+ #endregion
+
+ #region Private Classes
+
+ private class AsyncReceiveState
+ {
+ public AsyncReceiveState(Socket socket, EndPoint remoteEndPoint)
+ {
+ this.Socket = socket;
+ this.RemoteEndPoint = remoteEndPoint;
+ }
+
+ public EndPoint RemoteEndPoint;
+ public byte[] Buffer = new byte[8192];
+
+ public Socket Socket { get; private set; }
+
+ public TaskCompletionSource<SocketReceiveResult> TaskCompletionSource { get; set; }
+
+ }
+
+ #endregion
+
+ }
+}
diff --git a/Emby.Common.Implementations/Networking/NetworkManager.cs b/Emby.Common.Implementations/Networking/NetworkManager.cs
new file mode 100644
index 0000000000..4485e8b14e
--- /dev/null
+++ b/Emby.Common.Implementations/Networking/NetworkManager.cs
@@ -0,0 +1,525 @@
+using MediaBrowser.Model.Logging;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Net.Sockets;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Common.Net;
+
+namespace Emby.Common.Implementations.Networking
+{
+ public class NetworkManager : INetworkManager
+ {
+ protected ILogger Logger { get; private set; }
+ private DateTime _lastRefresh;
+
+ public NetworkManager(ILogger logger)
+ {
+ Logger = logger;
+ }
+
+ private List<IpAddressInfo> _localIpAddresses;
+ private readonly object _localIpAddressSyncLock = new object();
+
+ public List<IpAddressInfo> GetLocalIpAddresses()
+ {
+ const int cacheMinutes = 5;
+
+ lock (_localIpAddressSyncLock)
+ {
+ var forceRefresh = (DateTime.UtcNow - _lastRefresh).TotalMinutes >= cacheMinutes;
+
+ if (_localIpAddresses == null || forceRefresh)
+ {
+ var addresses = GetLocalIpAddressesInternal().Select(ToIpAddressInfo).ToList();
+
+ _localIpAddresses = addresses;
+ _lastRefresh = DateTime.UtcNow;
+
+ return addresses;
+ }
+ }
+
+ return _localIpAddresses;
+ }
+
+ private IEnumerable<IPAddress> GetLocalIpAddressesInternal()
+ {
+ var list = GetIPsDefault()
+ .ToList();
+
+ if (list.Count == 0)
+ {
+ list.AddRange(GetLocalIpAddressesFallback().Result);
+ }
+
+ return list.Where(FilterIpAddress).DistinctBy(i => i.ToString());
+ }
+
+ private bool FilterIpAddress(IPAddress address)
+ {
+ var addressString = address.ToString();
+
+ if (addressString.StartsWith("169.", StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ public bool IsInPrivateAddressSpace(string endpoint)
+ {
+ if (string.Equals(endpoint, "::1", StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+
+ // Handle ipv4 mapped to ipv6
+ endpoint = endpoint.Replace("::ffff:", string.Empty);
+
+ // Private address space:
+ // http://en.wikipedia.org/wiki/Private_network
+
+ if (endpoint.StartsWith("172.", StringComparison.OrdinalIgnoreCase))
+ {
+ return Is172AddressPrivate(endpoint);
+ }
+
+ return
+
+ endpoint.StartsWith("localhost", StringComparison.OrdinalIgnoreCase) ||
+ endpoint.StartsWith("127.", StringComparison.OrdinalIgnoreCase) ||
+ endpoint.StartsWith("10.", StringComparison.OrdinalIgnoreCase) ||
+ endpoint.StartsWith("192.168", StringComparison.OrdinalIgnoreCase) ||
+ endpoint.StartsWith("169.", StringComparison.OrdinalIgnoreCase);
+ }
+
+ private bool Is172AddressPrivate(string endpoint)
+ {
+ for (var i = 16; i <= 31; i++)
+ {
+ if (endpoint.StartsWith("172." + i.ToString(CultureInfo.InvariantCulture) + ".", StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public bool IsInLocalNetwork(string endpoint)
+ {
+ return IsInLocalNetworkInternal(endpoint, true);
+ }
+
+ public bool IsInLocalNetworkInternal(string endpoint, bool resolveHost)
+ {
+ if (string.IsNullOrWhiteSpace(endpoint))
+ {
+ throw new ArgumentNullException("endpoint");
+ }
+
+ IPAddress address;
+ if (IPAddress.TryParse(endpoint, out address))
+ {
+ var addressString = address.ToString();
+
+ int lengthMatch = 100;
+ if (address.AddressFamily == AddressFamily.InterNetwork)
+ {
+ lengthMatch = 4;
+ if (IsInPrivateAddressSpace(addressString))
+ {
+ return true;
+ }
+ }
+ else if (address.AddressFamily == AddressFamily.InterNetworkV6)
+ {
+ lengthMatch = 10;
+ if (IsInPrivateAddressSpace(endpoint))
+ {
+ return true;
+ }
+ }
+
+ // Should be even be doing this with ipv6?
+ if (addressString.Length >= lengthMatch)
+ {
+ var prefix = addressString.Substring(0, lengthMatch);
+
+ if (GetLocalIpAddresses().Any(i => i.ToString().StartsWith(prefix, StringComparison.OrdinalIgnoreCase)))
+ {
+ return true;
+ }
+ }
+ }
+ else if (resolveHost)
+ {
+ Uri uri;
+ if (Uri.TryCreate(endpoint, UriKind.RelativeOrAbsolute, out uri))
+ {
+ try
+ {
+ var host = uri.DnsSafeHost;
+ Logger.Debug("Resolving host {0}", host);
+
+ address = GetIpAddresses(host).Result.FirstOrDefault();
+
+ if (address != null)
+ {
+ Logger.Debug("{0} resolved to {1}", host, address);
+
+ return IsInLocalNetworkInternal(address.ToString(), false);
+ }
+ }
+ catch (InvalidOperationException)
+ {
+ // Can happen with reverse proxy or IIS url rewriting
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error resovling hostname", ex);
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private Task<IPAddress[]> GetIpAddresses(string hostName)
+ {
+ return Dns.GetHostAddressesAsync(hostName);
+ }
+
+ private readonly List<NetworkInterfaceType> _validNetworkInterfaceTypes = new List<NetworkInterfaceType>
+ {
+ NetworkInterfaceType.Ethernet,
+ NetworkInterfaceType.Wireless80211
+ };
+
+ private List<IPAddress> GetIPsDefault()
+ {
+ NetworkInterface[] interfaces;
+
+ try
+ {
+ var validStatuses = new[] { OperationalStatus.Up, OperationalStatus.Unknown };
+
+ interfaces = NetworkInterface.GetAllNetworkInterfaces()
+ .Where(i => validStatuses.Contains(i.OperationalStatus))
+ .ToArray();
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error in GetAllNetworkInterfaces", ex);
+ return new List<IPAddress>();
+ }
+
+ return interfaces.SelectMany(network =>
+ {
+
+ try
+ {
+ Logger.Debug("Querying interface: {0}. Type: {1}. Status: {2}", network.Name, network.NetworkInterfaceType, network.OperationalStatus);
+
+ var ipProperties = network.GetIPProperties();
+
+ // Try to exclude virtual adapters
+ // http://stackoverflow.com/questions/8089685/c-sharp-finding-my-machines-local-ip-address-and-not-the-vms
+ var addr = ipProperties.GatewayAddresses.FirstOrDefault();
+ if (addr == null|| string.Equals(addr.Address.ToString(), "0.0.0.0", StringComparison.OrdinalIgnoreCase))
+ {
+ return new List<IPAddress>();
+ }
+
+ //if (!_validNetworkInterfaceTypes.Contains(network.NetworkInterfaceType))
+ //{
+ // return new List<IPAddress>();
+ //}
+
+ return ipProperties.UnicastAddresses
+ //.Where(i => i.IsDnsEligible)
+ .Select(i => i.Address)
+ .Where(i => i.AddressFamily == AddressFamily.InterNetwork)
+ .ToList();
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error querying network interface", ex);
+ return new List<IPAddress>();
+ }
+
+ }).DistinctBy(i => i.ToString())
+ .ToList();
+ }
+
+ private async Task<IEnumerable<IPAddress>> GetLocalIpAddressesFallback()
+ {
+ var host = await Dns.GetHostEntryAsync(Dns.GetHostName()).ConfigureAwait(false);
+
+ // Reverse them because the last one is usually the correct one
+ // It's not fool-proof so ultimately the consumer will have to examine them and decide
+ return host.AddressList
+ .Where(i => i.AddressFamily == AddressFamily.InterNetwork)
+ .Reverse();
+ }
+
+ /// <summary>
+ /// Gets a random port number that is currently available
+ /// </summary>
+ /// <returns>System.Int32.</returns>
+ public int GetRandomUnusedPort()
+ {
+ var listener = new TcpListener(IPAddress.Any, 0);
+ listener.Start();
+ var port = ((IPEndPoint)listener.LocalEndpoint).Port;
+ listener.Stop();
+ return port;
+ }
+
+ /// <summary>
+ /// Returns MAC Address from first Network Card in Computer
+ /// </summary>
+ /// <returns>[string] MAC Address</returns>
+ public string GetMacAddress()
+ {
+ return NetworkInterface.GetAllNetworkInterfaces()
+ .Where(i => i.NetworkInterfaceType != NetworkInterfaceType.Loopback)
+ .Select(i => BitConverter.ToString(i.GetPhysicalAddress().GetAddressBytes()))
+ .FirstOrDefault();
+ }
+
+ /// <summary>
+ /// Parses the specified endpointstring.
+ /// </summary>
+ /// <param name="endpointstring">The endpointstring.</param>
+ /// <returns>IPEndPoint.</returns>
+ public IPEndPoint Parse(string endpointstring)
+ {
+ return Parse(endpointstring, -1).Result;
+ }
+
+ /// <summary>
+ /// Parses the specified endpointstring.
+ /// </summary>
+ /// <param name="endpointstring">The endpointstring.</param>
+ /// <param name="defaultport">The defaultport.</param>
+ /// <returns>IPEndPoint.</returns>
+ /// <exception cref="System.ArgumentException">Endpoint descriptor may not be empty.</exception>
+ /// <exception cref="System.FormatException"></exception>
+ private static async Task<IPEndPoint> Parse(string endpointstring, int defaultport)
+ {
+ if (String.IsNullOrEmpty(endpointstring)
+ || endpointstring.Trim().Length == 0)
+ {
+ throw new ArgumentException("Endpoint descriptor may not be empty.");
+ }
+
+ if (defaultport != -1 &&
+ (defaultport < IPEndPoint.MinPort
+ || defaultport > IPEndPoint.MaxPort))
+ {
+ throw new ArgumentException(String.Format("Invalid default port '{0}'", defaultport));
+ }
+
+ string[] values = endpointstring.Split(new char[] { ':' });
+ IPAddress ipaddy;
+ int port = -1;
+
+ //check if we have an IPv6 or ports
+ if (values.Length <= 2) // ipv4 or hostname
+ {
+ port = values.Length == 1 ? defaultport : GetPort(values[1]);
+
+ //try to use the address as IPv4, otherwise get hostname
+ if (!IPAddress.TryParse(values[0], out ipaddy))
+ ipaddy = await GetIPfromHost(values[0]).ConfigureAwait(false);
+ }
+ else if (values.Length > 2) //ipv6
+ {
+ //could [a:b:c]:d
+ if (values[0].StartsWith("[") && values[values.Length - 2].EndsWith("]"))
+ {
+ string ipaddressstring = String.Join(":", values.Take(values.Length - 1).ToArray());
+ ipaddy = IPAddress.Parse(ipaddressstring);
+ port = GetPort(values[values.Length - 1]);
+ }
+ else //[a:b:c] or a:b:c
+ {
+ ipaddy = IPAddress.Parse(endpointstring);
+ port = defaultport;
+ }
+ }
+ else
+ {
+ throw new FormatException(String.Format("Invalid endpoint ipaddress '{0}'", endpointstring));
+ }
+
+ if (port == -1)
+ throw new ArgumentException(String.Format("No port specified: '{0}'", endpointstring));
+
+ return new IPEndPoint(ipaddy, port);
+ }
+
+ protected static readonly CultureInfo UsCulture = new CultureInfo("en-US");
+
+ /// <summary>
+ /// Gets the port.
+ /// </summary>
+ /// <param name="p">The p.</param>
+ /// <returns>System.Int32.</returns>
+ /// <exception cref="System.FormatException"></exception>
+ private static int GetPort(string p)
+ {
+ int port;
+
+ if (!Int32.TryParse(p, out port)
+ || port < IPEndPoint.MinPort
+ || port > IPEndPoint.MaxPort)
+ {
+ throw new FormatException(String.Format("Invalid end point port '{0}'", p));
+ }
+
+ return port;
+ }
+
+ /// <summary>
+ /// Gets the I pfrom host.
+ /// </summary>
+ /// <param name="p">The p.</param>
+ /// <returns>IPAddress.</returns>
+ /// <exception cref="System.ArgumentException"></exception>
+ private static async Task<IPAddress> GetIPfromHost(string p)
+ {
+ var hosts = await Dns.GetHostAddressesAsync(p).ConfigureAwait(false);
+
+ if (hosts == null || hosts.Length == 0)
+ throw new ArgumentException(String.Format("Host not found: {0}", p));
+
+ return hosts[0];
+ }
+
+ public IpAddressInfo ParseIpAddress(string ipAddress)
+ {
+ IpAddressInfo info;
+ if (TryParseIpAddress(ipAddress, out info))
+ {
+ return info;
+ }
+
+ throw new ArgumentException("Invalid ip address: " + ipAddress);
+ }
+
+ public bool TryParseIpAddress(string ipAddress, out IpAddressInfo ipAddressInfo)
+ {
+ IPAddress address;
+ if (IPAddress.TryParse(ipAddress, out address))
+ {
+ ipAddressInfo = ToIpAddressInfo(address);
+ return true;
+ }
+
+ ipAddressInfo = null;
+ return false;
+ }
+
+ public static IpEndPointInfo ToIpEndPointInfo(IPEndPoint endpoint)
+ {
+ if (endpoint == null)
+ {
+ return null;
+ }
+
+ return new IpEndPointInfo(ToIpAddressInfo(endpoint.Address), endpoint.Port);
+ }
+
+ public static IPEndPoint ToIPEndPoint(IpEndPointInfo endpoint)
+ {
+ if (endpoint == null)
+ {
+ return null;
+ }
+
+ return new IPEndPoint(ToIPAddress(endpoint.IpAddress), endpoint.Port);
+ }
+
+ public static IPAddress ToIPAddress(IpAddressInfo address)
+ {
+ if (address.Equals(IpAddressInfo.Any))
+ {
+ return IPAddress.Any;
+ }
+ if (address.Equals(IpAddressInfo.IPv6Any))
+ {
+ return IPAddress.IPv6Any;
+ }
+ if (address.Equals(IpAddressInfo.Loopback))
+ {
+ return IPAddress.Loopback;
+ }
+ if (address.Equals(IpAddressInfo.IPv6Loopback))
+ {
+ return IPAddress.IPv6Loopback;
+ }
+
+ return IPAddress.Parse(address.Address);
+ }
+
+ public static IpAddressInfo ToIpAddressInfo(IPAddress address)
+ {
+ if (address.Equals(IPAddress.Any))
+ {
+ return IpAddressInfo.Any;
+ }
+ if (address.Equals(IPAddress.IPv6Any))
+ {
+ return IpAddressInfo.IPv6Any;
+ }
+ if (address.Equals(IPAddress.Loopback))
+ {
+ return IpAddressInfo.Loopback;
+ }
+ if (address.Equals(IPAddress.IPv6Loopback))
+ {
+ return IpAddressInfo.IPv6Loopback;
+ }
+ return new IpAddressInfo
+ {
+ Address = address.ToString(),
+ AddressFamily = address.AddressFamily == AddressFamily.InterNetworkV6 ? IpAddressFamily.InterNetworkV6 : IpAddressFamily.InterNetwork
+ };
+ }
+
+ public async Task<IpAddressInfo[]> GetHostAddressesAsync(string host)
+ {
+ var addresses = await Dns.GetHostAddressesAsync(host).ConfigureAwait(false);
+ return addresses.Select(ToIpAddressInfo).ToArray();
+ }
+
+ /// <summary>
+ /// Gets the network shares.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>IEnumerable{NetworkShare}.</returns>
+ public virtual IEnumerable<NetworkShare> GetNetworkShares(string path)
+ {
+ return new List<NetworkShare>();
+ }
+
+ /// <summary>
+ /// Gets available devices within the domain
+ /// </summary>
+ /// <returns>PC's in the Domain</returns>
+ public virtual IEnumerable<FileSystemEntryInfo> GetNetworkDevices()
+ {
+ return new List<FileSystemEntryInfo>();
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/Properties/AssemblyInfo.cs b/Emby.Common.Implementations/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..1a5abcb274
--- /dev/null
+++ b/Emby.Common.Implementations/Properties/AssemblyInfo.cs
@@ -0,0 +1,19 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// 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: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("Emby.Common.Implementations")]
+[assembly: AssemblyTrademark("")]
+
+// 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("5a27010a-09c6-4e86-93ea-437484c10917")]
diff --git a/Emby.Common.Implementations/Reflection/AssemblyInfo.cs b/Emby.Common.Implementations/Reflection/AssemblyInfo.cs
new file mode 100644
index 0000000000..7a92f02d6b
--- /dev/null
+++ b/Emby.Common.Implementations/Reflection/AssemblyInfo.cs
@@ -0,0 +1,31 @@
+using System;
+using System.IO;
+using MediaBrowser.Model.Reflection;
+using System.Reflection;
+
+namespace Emby.Common.Implementations.Reflection
+{
+ public class AssemblyInfo : IAssemblyInfo
+ {
+ public Stream GetManifestResourceStream(Type type, string resource)
+ {
+#if NET46
+ return type.Assembly.GetManifestResourceStream(resource);
+#endif
+ return type.GetTypeInfo().Assembly.GetManifestResourceStream(resource);
+ }
+
+ public string[] GetManifestResourceNames(Type type)
+ {
+#if NET46
+ return type.Assembly.GetManifestResourceNames();
+#endif
+ return type.GetTypeInfo().Assembly.GetManifestResourceNames();
+ }
+
+ public Assembly[] GetCurrentAssemblies()
+ {
+ return AppDomain.CurrentDomain.GetAssemblies();
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/ScheduledTasks/DailyTrigger.cs b/Emby.Common.Implementations/ScheduledTasks/DailyTrigger.cs
new file mode 100644
index 0000000000..5735f80260
--- /dev/null
+++ b/Emby.Common.Implementations/ScheduledTasks/DailyTrigger.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Globalization;
+using System.Threading;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Tasks;
+
+namespace Emby.Common.Implementations.ScheduledTasks
+{
+ /// <summary>
+ /// Represents a task trigger that fires everyday
+ /// </summary>
+ public class DailyTrigger : ITaskTrigger
+ {
+ /// <summary>
+ /// Get the time of day to trigger the task to run
+ /// </summary>
+ /// <value>The time of day.</value>
+ public TimeSpan TimeOfDay { get; set; }
+
+ /// <summary>
+ /// Gets or sets the timer.
+ /// </summary>
+ /// <value>The timer.</value>
+ private Timer Timer { get; set; }
+
+ /// <summary>
+ /// Gets the execution properties of this task.
+ /// </summary>
+ /// <value>
+ /// The execution properties of this task.
+ /// </value>
+ public TaskExecutionOptions TaskOptions { get; set; }
+
+ /// <summary>
+ /// Stars waiting for the trigger action
+ /// </summary>
+ /// <param name="lastResult">The last result.</param>
+ /// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param>
+ public void Start(TaskResult lastResult, ILogger logger, string taskName, bool isApplicationStartup)
+ {
+ DisposeTimer();
+
+ var now = DateTime.Now;
+
+ var triggerDate = now.TimeOfDay > TimeOfDay ? now.Date.AddDays(1) : now.Date;
+ triggerDate = triggerDate.Add(TimeOfDay);
+
+ var dueTime = triggerDate - now;
+
+ logger.Info("Daily trigger for {0} set to fire at {1}, which is {2} minutes from now.", taskName, triggerDate.ToString(), dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture));
+
+ Timer = new Timer(state => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1));
+ }
+
+ /// <summary>
+ /// Stops waiting for the trigger action
+ /// </summary>
+ public void Stop()
+ {
+ DisposeTimer();
+ }
+
+ /// <summary>
+ /// Disposes the timer.
+ /// </summary>
+ private void DisposeTimer()
+ {
+ if (Timer != null)
+ {
+ Timer.Dispose();
+ }
+ }
+
+ /// <summary>
+ /// Occurs when [triggered].
+ /// </summary>
+ public event EventHandler<GenericEventArgs<TaskExecutionOptions>> Triggered;
+
+ /// <summary>
+ /// Called when [triggered].
+ /// </summary>
+ private void OnTriggered()
+ {
+ if (Triggered != null)
+ {
+ Triggered(this, new GenericEventArgs<TaskExecutionOptions>(TaskOptions));
+ }
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/ScheduledTasks/IntervalTrigger.cs b/Emby.Common.Implementations/ScheduledTasks/IntervalTrigger.cs
new file mode 100644
index 0000000000..4d2769d8fb
--- /dev/null
+++ b/Emby.Common.Implementations/ScheduledTasks/IntervalTrigger.cs
@@ -0,0 +1,112 @@
+using System;
+using System.Linq;
+using System.Threading;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Tasks;
+
+namespace Emby.Common.Implementations.ScheduledTasks
+{
+ /// <summary>
+ /// Represents a task trigger that runs repeatedly on an interval
+ /// </summary>
+ public class IntervalTrigger : ITaskTrigger
+ {
+ /// <summary>
+ /// Gets or sets the interval.
+ /// </summary>
+ /// <value>The interval.</value>
+ public TimeSpan Interval { get; set; }
+
+ /// <summary>
+ /// Gets or sets the timer.
+ /// </summary>
+ /// <value>The timer.</value>
+ private Timer Timer { get; set; }
+
+ /// <summary>
+ /// Gets the execution properties of this task.
+ /// </summary>
+ /// <value>
+ /// The execution properties of this task.
+ /// </value>
+ public TaskExecutionOptions TaskOptions { get; set; }
+
+ private DateTime _lastStartDate;
+
+ /// <summary>
+ /// Stars waiting for the trigger action
+ /// </summary>
+ /// <param name="lastResult">The last result.</param>
+ /// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param>
+ public void Start(TaskResult lastResult, ILogger logger, string taskName, bool isApplicationStartup)
+ {
+ DisposeTimer();
+
+ DateTime triggerDate;
+
+ if (lastResult == null)
+ {
+ // Task has never been completed before
+ triggerDate = DateTime.UtcNow.AddHours(1);
+ }
+ else
+ {
+ triggerDate = new[] { lastResult.EndTimeUtc, _lastStartDate }.Max().Add(Interval);
+ }
+
+ if (DateTime.UtcNow > triggerDate)
+ {
+ triggerDate = DateTime.UtcNow.AddMinutes(1);
+ }
+
+ var dueTime = triggerDate - DateTime.UtcNow;
+ var maxDueTime = TimeSpan.FromDays(7);
+
+ if (dueTime > maxDueTime)
+ {
+ dueTime = maxDueTime;
+ }
+
+ Timer = new Timer(state => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1));
+ }
+
+ /// <summary>
+ /// Stops waiting for the trigger action
+ /// </summary>
+ public void Stop()
+ {
+ DisposeTimer();
+ }
+
+ /// <summary>
+ /// Disposes the timer.
+ /// </summary>
+ private void DisposeTimer()
+ {
+ if (Timer != null)
+ {
+ Timer.Dispose();
+ }
+ }
+
+ /// <summary>
+ /// Occurs when [triggered].
+ /// </summary>
+ public event EventHandler<GenericEventArgs<TaskExecutionOptions>> Triggered;
+
+ /// <summary>
+ /// Called when [triggered].
+ /// </summary>
+ private void OnTriggered()
+ {
+ DisposeTimer();
+
+ if (Triggered != null)
+ {
+ _lastStartDate = DateTime.UtcNow;
+ Triggered(this, new GenericEventArgs<TaskExecutionOptions>(TaskOptions));
+ }
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
new file mode 100644
index 0000000000..cbc7c7c2d8
--- /dev/null
+++ b/Emby.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
@@ -0,0 +1,783 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Events;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Model.System;
+using MediaBrowser.Model.Tasks;
+
+namespace Emby.Common.Implementations.ScheduledTasks
+{
+ /// <summary>
+ /// Class ScheduledTaskWorker
+ /// </summary>
+ public class ScheduledTaskWorker : IScheduledTaskWorker
+ {
+ public event EventHandler<GenericEventArgs<double>> TaskProgress;
+
+ /// <summary>
+ /// Gets or sets the scheduled task.
+ /// </summary>
+ /// <value>The scheduled task.</value>
+ public IScheduledTask ScheduledTask { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the json serializer.
+ /// </summary>
+ /// <value>The json serializer.</value>
+ private IJsonSerializer JsonSerializer { get; set; }
+
+ /// <summary>
+ /// Gets or sets the application paths.
+ /// </summary>
+ /// <value>The application paths.</value>
+ private IApplicationPaths ApplicationPaths { get; set; }
+
+ /// <summary>
+ /// Gets the logger.
+ /// </summary>
+ /// <value>The logger.</value>
+ private ILogger Logger { get; set; }
+
+ /// <summary>
+ /// Gets the task manager.
+ /// </summary>
+ /// <value>The task manager.</value>
+ private ITaskManager TaskManager { get; set; }
+ private readonly IFileSystem _fileSystem;
+ private readonly ISystemEvents _systemEvents;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ScheduledTaskWorker" /> class.
+ /// </summary>
+ /// <param name="scheduledTask">The scheduled task.</param>
+ /// <param name="applicationPaths">The application paths.</param>
+ /// <param name="taskManager">The task manager.</param>
+ /// <param name="jsonSerializer">The json serializer.</param>
+ /// <param name="logger">The logger.</param>
+ /// <exception cref="System.ArgumentNullException">
+ /// scheduledTask
+ /// or
+ /// applicationPaths
+ /// or
+ /// taskManager
+ /// or
+ /// jsonSerializer
+ /// or
+ /// logger
+ /// </exception>
+ public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, IJsonSerializer jsonSerializer, ILogger logger, IFileSystem fileSystem, ISystemEvents systemEvents)
+ {
+ if (scheduledTask == null)
+ {
+ throw new ArgumentNullException("scheduledTask");
+ }
+ if (applicationPaths == null)
+ {
+ throw new ArgumentNullException("applicationPaths");
+ }
+ if (taskManager == null)
+ {
+ throw new ArgumentNullException("taskManager");
+ }
+ if (jsonSerializer == null)
+ {
+ throw new ArgumentNullException("jsonSerializer");
+ }
+ if (logger == null)
+ {
+ throw new ArgumentNullException("logger");
+ }
+
+ ScheduledTask = scheduledTask;
+ ApplicationPaths = applicationPaths;
+ TaskManager = taskManager;
+ JsonSerializer = jsonSerializer;
+ Logger = logger;
+ _fileSystem = fileSystem;
+ _systemEvents = systemEvents;
+
+ InitTriggerEvents();
+ }
+
+ private bool _readFromFile = false;
+ /// <summary>
+ /// The _last execution result
+ /// </summary>
+ private TaskResult _lastExecutionResult;
+ /// <summary>
+ /// The _last execution result sync lock
+ /// </summary>
+ private readonly object _lastExecutionResultSyncLock = new object();
+ /// <summary>
+ /// Gets the last execution result.
+ /// </summary>
+ /// <value>The last execution result.</value>
+ public TaskResult LastExecutionResult
+ {
+ get
+ {
+ var path = GetHistoryFilePath();
+
+ lock (_lastExecutionResultSyncLock)
+ {
+ if (_lastExecutionResult == null && !_readFromFile)
+ {
+ try
+ {
+ _lastExecutionResult = JsonSerializer.DeserializeFromFile<TaskResult>(path);
+ }
+ catch (DirectoryNotFoundException)
+ {
+ // File doesn't exist. No biggie
+ }
+ catch (FileNotFoundException)
+ {
+ // File doesn't exist. No biggie
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error deserializing {0}", ex, path);
+ }
+ _readFromFile = true;
+ }
+ }
+
+ return _lastExecutionResult;
+ }
+ private set
+ {
+ _lastExecutionResult = value;
+
+ var path = GetHistoryFilePath();
+ _fileSystem.CreateDirectory(Path.GetDirectoryName(path));
+
+ lock (_lastExecutionResultSyncLock)
+ {
+ JsonSerializer.SerializeToFile(value, path);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return ScheduledTask.Name; }
+ }
+
+ /// <summary>
+ /// Gets the description.
+ /// </summary>
+ /// <value>The description.</value>
+ public string Description
+ {
+ get { return ScheduledTask.Description; }
+ }
+
+ /// <summary>
+ /// Gets the category.
+ /// </summary>
+ /// <value>The category.</value>
+ public string Category
+ {
+ get { return ScheduledTask.Category; }
+ }
+
+ /// <summary>
+ /// Gets the current cancellation token
+ /// </summary>
+ /// <value>The current cancellation token source.</value>
+ private CancellationTokenSource CurrentCancellationTokenSource { get; set; }
+
+ /// <summary>
+ /// Gets or sets the current execution start time.
+ /// </summary>
+ /// <value>The current execution start time.</value>
+ private DateTime CurrentExecutionStartTime { get; set; }
+
+ /// <summary>
+ /// Gets the state.
+ /// </summary>
+ /// <value>The state.</value>
+ public TaskState State
+ {
+ get
+ {
+ if (CurrentCancellationTokenSource != null)
+ {
+ return CurrentCancellationTokenSource.IsCancellationRequested
+ ? TaskState.Cancelling
+ : TaskState.Running;
+ }
+
+ return TaskState.Idle;
+ }
+ }
+
+ /// <summary>
+ /// Gets the current progress.
+ /// </summary>
+ /// <value>The current progress.</value>
+ public double? CurrentProgress { get; private set; }
+
+ /// <summary>
+ /// The _triggers
+ /// </summary>
+ private Tuple<TaskTriggerInfo,ITaskTrigger>[] _triggers;
+ /// <summary>
+ /// Gets the triggers that define when the task will run
+ /// </summary>
+ /// <value>The triggers.</value>
+ private Tuple<TaskTriggerInfo, ITaskTrigger>[] InternalTriggers
+ {
+ get
+ {
+ return _triggers;
+ }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+
+ // Cleanup current triggers
+ if (_triggers != null)
+ {
+ DisposeTriggers();
+ }
+
+ _triggers = value.ToArray();
+
+ ReloadTriggerEvents(false);
+ }
+ }
+
+ /// <summary>
+ /// Gets the triggers that define when the task will run
+ /// </summary>
+ /// <value>The triggers.</value>
+ /// <exception cref="System.ArgumentNullException">value</exception>
+ public TaskTriggerInfo[] Triggers
+ {
+ get
+ {
+ return InternalTriggers.Select(i => i.Item1).ToArray();
+ }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+
+ SaveTriggers(value);
+
+ InternalTriggers = value.Select(i => new Tuple<TaskTriggerInfo, ITaskTrigger>(i, GetTrigger(i))).ToArray();
+ }
+ }
+
+ /// <summary>
+ /// The _id
+ /// </summary>
+ private string _id;
+
+ /// <summary>
+ /// Gets the unique id.
+ /// </summary>
+ /// <value>The unique id.</value>
+ public string Id
+ {
+ get
+ {
+ if (_id == null)
+ {
+ _id = ScheduledTask.GetType().FullName.GetMD5().ToString("N");
+ }
+
+ return _id;
+ }
+ }
+
+ private void InitTriggerEvents()
+ {
+ _triggers = LoadTriggers();
+ ReloadTriggerEvents(true);
+ }
+
+ public void ReloadTriggerEvents()
+ {
+ ReloadTriggerEvents(false);
+ }
+
+ /// <summary>
+ /// Reloads the trigger events.
+ /// </summary>
+ /// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param>
+ private void ReloadTriggerEvents(bool isApplicationStartup)
+ {
+ foreach (var triggerInfo in InternalTriggers)
+ {
+ var trigger = triggerInfo.Item2;
+
+ trigger.Stop();
+
+ trigger.Triggered -= trigger_Triggered;
+ trigger.Triggered += trigger_Triggered;
+ trigger.Start(LastExecutionResult, Logger, Name, isApplicationStartup);
+ }
+ }
+
+ /// <summary>
+ /// Handles the Triggered event of the trigger control.
+ /// </summary>
+ /// <param name="sender">The source of the event.</param>
+ /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
+ async void trigger_Triggered(object sender, GenericEventArgs<TaskExecutionOptions> e)
+ {
+ var trigger = (ITaskTrigger)sender;
+
+ var configurableTask = ScheduledTask as IConfigurableScheduledTask;
+
+ if (configurableTask != null && !configurableTask.IsEnabled)
+ {
+ return;
+ }
+
+ Logger.Info("{0} fired for task: {1}", trigger.GetType().Name, Name);
+
+ trigger.Stop();
+
+ TaskManager.QueueScheduledTask(ScheduledTask, e.Argument);
+
+ await Task.Delay(1000).ConfigureAwait(false);
+
+ trigger.Start(LastExecutionResult, Logger, Name, false);
+ }
+
+ private Task _currentTask;
+
+ /// <summary>
+ /// Executes the task
+ /// </summary>
+ /// <param name="options">Task options.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.InvalidOperationException">Cannot execute a Task that is already running</exception>
+ public async Task Execute(TaskExecutionOptions options)
+ {
+ var task = ExecuteInternal(options);
+
+ _currentTask = task;
+
+ try
+ {
+ await task.ConfigureAwait(false);
+ }
+ finally
+ {
+ _currentTask = null;
+ GC.Collect();
+ }
+ }
+
+ private async Task ExecuteInternal(TaskExecutionOptions options)
+ {
+ // Cancel the current execution, if any
+ if (CurrentCancellationTokenSource != null)
+ {
+ throw new InvalidOperationException("Cannot execute a Task that is already running");
+ }
+
+ var progress = new Progress<double>();
+
+ CurrentCancellationTokenSource = new CancellationTokenSource();
+
+ Logger.Info("Executing {0}", Name);
+
+ ((TaskManager)TaskManager).OnTaskExecuting(this);
+
+ progress.ProgressChanged += progress_ProgressChanged;
+
+ TaskCompletionStatus status;
+ CurrentExecutionStartTime = DateTime.UtcNow;
+
+ Exception failureException = null;
+
+ try
+ {
+ if (options != null && options.MaxRuntimeMs.HasValue)
+ {
+ CurrentCancellationTokenSource.CancelAfter(options.MaxRuntimeMs.Value);
+ }
+
+ var localTask = ScheduledTask.Execute(CurrentCancellationTokenSource.Token, progress);
+
+ await localTask.ConfigureAwait(false);
+
+ status = TaskCompletionStatus.Completed;
+ }
+ catch (OperationCanceledException)
+ {
+ status = TaskCompletionStatus.Cancelled;
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error", ex);
+
+ failureException = ex;
+
+ status = TaskCompletionStatus.Failed;
+ }
+
+ var startTime = CurrentExecutionStartTime;
+ var endTime = DateTime.UtcNow;
+
+ progress.ProgressChanged -= progress_ProgressChanged;
+ CurrentCancellationTokenSource.Dispose();
+ CurrentCancellationTokenSource = null;
+ CurrentProgress = null;
+
+ OnTaskCompleted(startTime, endTime, status, failureException);
+ }
+
+ /// <summary>
+ /// Progress_s the progress changed.
+ /// </summary>
+ /// <param name="sender">The sender.</param>
+ /// <param name="e">The e.</param>
+ void progress_ProgressChanged(object sender, double e)
+ {
+ CurrentProgress = e;
+
+ EventHelper.FireEventIfNotNull(TaskProgress, this, new GenericEventArgs<double>
+ {
+ Argument = e
+
+ }, Logger);
+ }
+
+ /// <summary>
+ /// Stops the task if it is currently executing
+ /// </summary>
+ /// <exception cref="System.InvalidOperationException">Cannot cancel a Task unless it is in the Running state.</exception>
+ public void Cancel()
+ {
+ if (State != TaskState.Running)
+ {
+ throw new InvalidOperationException("Cannot cancel a Task unless it is in the Running state.");
+ }
+
+ CancelIfRunning();
+ }
+
+ /// <summary>
+ /// Cancels if running.
+ /// </summary>
+ public void CancelIfRunning()
+ {
+ if (State == TaskState.Running)
+ {
+ Logger.Info("Attempting to cancel Scheduled Task {0}", Name);
+ CurrentCancellationTokenSource.Cancel();
+ }
+ }
+
+ /// <summary>
+ /// Gets the scheduled tasks configuration directory.
+ /// </summary>
+ /// <returns>System.String.</returns>
+ private string GetScheduledTasksConfigurationDirectory()
+ {
+ return Path.Combine(ApplicationPaths.ConfigurationDirectoryPath, "ScheduledTasks");
+ }
+
+ /// <summary>
+ /// Gets the scheduled tasks data directory.
+ /// </summary>
+ /// <returns>System.String.</returns>
+ private string GetScheduledTasksDataDirectory()
+ {
+ return Path.Combine(ApplicationPaths.DataPath, "ScheduledTasks");
+ }
+
+ /// <summary>
+ /// Gets the history file path.
+ /// </summary>
+ /// <value>The history file path.</value>
+ private string GetHistoryFilePath()
+ {
+ return Path.Combine(GetScheduledTasksDataDirectory(), new Guid(Id) + ".js");
+ }
+
+ /// <summary>
+ /// Gets the configuration file path.
+ /// </summary>
+ /// <returns>System.String.</returns>
+ private string GetConfigurationFilePath()
+ {
+ return Path.Combine(GetScheduledTasksConfigurationDirectory(), new Guid(Id) + ".js");
+ }
+
+ /// <summary>
+ /// Loads the triggers.
+ /// </summary>
+ /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
+ private Tuple<TaskTriggerInfo, ITaskTrigger>[] LoadTriggers()
+ {
+ var settings = LoadTriggerSettings();
+
+ return settings.Select(i => new Tuple<TaskTriggerInfo, ITaskTrigger>(i, GetTrigger(i))).ToArray();
+ }
+
+ private TaskTriggerInfo[] LoadTriggerSettings()
+ {
+ try
+ {
+ return JsonSerializer.DeserializeFromFile<IEnumerable<TaskTriggerInfo>>(GetConfigurationFilePath())
+ .ToArray();
+ }
+ catch (FileNotFoundException)
+ {
+ // File doesn't exist. No biggie. Return defaults.
+ return ScheduledTask.GetDefaultTriggers().ToArray();
+ }
+ catch (DirectoryNotFoundException)
+ {
+ // File doesn't exist. No biggie. Return defaults.
+ return ScheduledTask.GetDefaultTriggers().ToArray();
+ }
+ }
+
+ /// <summary>
+ /// Saves the triggers.
+ /// </summary>
+ /// <param name="triggers">The triggers.</param>
+ private void SaveTriggers(TaskTriggerInfo[] triggers)
+ {
+ var path = GetConfigurationFilePath();
+
+ _fileSystem.CreateDirectory(Path.GetDirectoryName(path));
+
+ JsonSerializer.SerializeToFile(triggers, path);
+ }
+
+ /// <summary>
+ /// Called when [task completed].
+ /// </summary>
+ /// <param name="startTime">The start time.</param>
+ /// <param name="endTime">The end time.</param>
+ /// <param name="status">The status.</param>
+ private void OnTaskCompleted(DateTime startTime, DateTime endTime, TaskCompletionStatus status, Exception ex)
+ {
+ var elapsedTime = endTime - startTime;
+
+ Logger.Info("{0} {1} after {2} minute(s) and {3} seconds", Name, status, Math.Truncate(elapsedTime.TotalMinutes), elapsedTime.Seconds);
+
+ var result = new TaskResult
+ {
+ StartTimeUtc = startTime,
+ EndTimeUtc = endTime,
+ Status = status,
+ Name = Name,
+ Id = Id
+ };
+
+ result.Key = ScheduledTask.Key;
+
+ if (ex != null)
+ {
+ result.ErrorMessage = ex.Message;
+ result.LongErrorMessage = ex.StackTrace;
+ }
+
+ LastExecutionResult = result;
+
+ ((TaskManager)TaskManager).OnTaskCompleted(this, result);
+ }
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources.
+ /// </summary>
+ /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool dispose)
+ {
+ if (dispose)
+ {
+ DisposeTriggers();
+
+ var wassRunning = State == TaskState.Running;
+ var startTime = CurrentExecutionStartTime;
+
+ var token = CurrentCancellationTokenSource;
+ if (token != null)
+ {
+ try
+ {
+ Logger.Info(Name + ": Cancelling");
+ token.Cancel();
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error calling CancellationToken.Cancel();", ex);
+ }
+ }
+ var task = _currentTask;
+ if (task != null)
+ {
+ try
+ {
+ Logger.Info(Name + ": Waiting on Task");
+ var exited = Task.WaitAll(new[] { task }, 2000);
+
+ if (exited)
+ {
+ Logger.Info(Name + ": Task exited");
+ }
+ else
+ {
+ Logger.Info(Name + ": Timed out waiting for task to stop");
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error calling Task.WaitAll();", ex);
+ }
+ }
+
+ if (token != null)
+ {
+ try
+ {
+ Logger.Debug(Name + ": Disposing CancellationToken");
+ token.Dispose();
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error calling CancellationToken.Dispose();", ex);
+ }
+ }
+ if (wassRunning)
+ {
+ OnTaskCompleted(startTime, DateTime.UtcNow, TaskCompletionStatus.Aborted, null);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Converts a TaskTriggerInfo into a concrete BaseTaskTrigger
+ /// </summary>
+ /// <param name="info">The info.</param>
+ /// <returns>BaseTaskTrigger.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ /// <exception cref="System.ArgumentException">Invalid trigger type: + info.Type</exception>
+ private ITaskTrigger GetTrigger(TaskTriggerInfo info)
+ {
+ var options = new TaskExecutionOptions
+ {
+ MaxRuntimeMs = info.MaxRuntimeMs
+ };
+
+ if (info.Type.Equals(typeof(DailyTrigger).Name, StringComparison.OrdinalIgnoreCase))
+ {
+ if (!info.TimeOfDayTicks.HasValue)
+ {
+ throw new ArgumentNullException();
+ }
+
+ return new DailyTrigger
+ {
+ TimeOfDay = TimeSpan.FromTicks(info.TimeOfDayTicks.Value),
+ TaskOptions = options
+ };
+ }
+
+ if (info.Type.Equals(typeof(WeeklyTrigger).Name, StringComparison.OrdinalIgnoreCase))
+ {
+ if (!info.TimeOfDayTicks.HasValue)
+ {
+ throw new ArgumentNullException();
+ }
+
+ if (!info.DayOfWeek.HasValue)
+ {
+ throw new ArgumentNullException();
+ }
+
+ return new WeeklyTrigger
+ {
+ TimeOfDay = TimeSpan.FromTicks(info.TimeOfDayTicks.Value),
+ DayOfWeek = info.DayOfWeek.Value,
+ TaskOptions = options
+ };
+ }
+
+ if (info.Type.Equals(typeof(IntervalTrigger).Name, StringComparison.OrdinalIgnoreCase))
+ {
+ if (!info.IntervalTicks.HasValue)
+ {
+ throw new ArgumentNullException();
+ }
+
+ return new IntervalTrigger
+ {
+ Interval = TimeSpan.FromTicks(info.IntervalTicks.Value),
+ TaskOptions = options
+ };
+ }
+
+ if (info.Type.Equals(typeof(SystemEventTrigger).Name, StringComparison.OrdinalIgnoreCase))
+ {
+ if (!info.SystemEvent.HasValue)
+ {
+ throw new ArgumentNullException();
+ }
+
+ return new SystemEventTrigger(_systemEvents)
+ {
+ SystemEvent = info.SystemEvent.Value,
+ TaskOptions = options
+ };
+ }
+
+ if (info.Type.Equals(typeof(StartupTrigger).Name, StringComparison.OrdinalIgnoreCase))
+ {
+ return new StartupTrigger();
+ }
+
+ throw new ArgumentException("Unrecognized trigger type: " + info.Type);
+ }
+
+ /// <summary>
+ /// Disposes each trigger
+ /// </summary>
+ private void DisposeTriggers()
+ {
+ foreach (var triggerInfo in InternalTriggers)
+ {
+ var trigger = triggerInfo.Item2;
+ trigger.Triggered -= trigger_Triggered;
+ trigger.Stop();
+ }
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/ScheduledTasks/StartupTrigger.cs b/Emby.Common.Implementations/ScheduledTasks/StartupTrigger.cs
new file mode 100644
index 0000000000..8aae644bc9
--- /dev/null
+++ b/Emby.Common.Implementations/ScheduledTasks/StartupTrigger.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Tasks;
+
+namespace Emby.Common.Implementations.ScheduledTasks
+{
+ /// <summary>
+ /// Class StartupTaskTrigger
+ /// </summary>
+ public class StartupTrigger : ITaskTrigger
+ {
+ public int DelayMs { get; set; }
+
+ /// <summary>
+ /// Gets the execution properties of this task.
+ /// </summary>
+ /// <value>
+ /// The execution properties of this task.
+ /// </value>
+ public TaskExecutionOptions TaskOptions { get; set; }
+
+ public StartupTrigger()
+ {
+ DelayMs = 3000;
+ }
+
+ /// <summary>
+ /// Stars waiting for the trigger action
+ /// </summary>
+ /// <param name="lastResult">The last result.</param>
+ /// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param>
+ public async void Start(TaskResult lastResult, ILogger logger, string taskName, bool isApplicationStartup)
+ {
+ if (isApplicationStartup)
+ {
+ await Task.Delay(DelayMs).ConfigureAwait(false);
+
+ OnTriggered();
+ }
+ }
+
+ /// <summary>
+ /// Stops waiting for the trigger action
+ /// </summary>
+ public void Stop()
+ {
+ }
+
+ /// <summary>
+ /// Occurs when [triggered].
+ /// </summary>
+ public event EventHandler<GenericEventArgs<TaskExecutionOptions>> Triggered;
+
+ /// <summary>
+ /// Called when [triggered].
+ /// </summary>
+ private void OnTriggered()
+ {
+ if (Triggered != null)
+ {
+ Triggered(this, new GenericEventArgs<TaskExecutionOptions>(TaskOptions));
+ }
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/ScheduledTasks/SystemEventTrigger.cs b/Emby.Common.Implementations/ScheduledTasks/SystemEventTrigger.cs
new file mode 100644
index 0000000000..a136a975ae
--- /dev/null
+++ b/Emby.Common.Implementations/ScheduledTasks/SystemEventTrigger.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.System;
+using MediaBrowser.Model.Tasks;
+
+namespace Emby.Common.Implementations.ScheduledTasks
+{
+ /// <summary>
+ /// Class SystemEventTrigger
+ /// </summary>
+ public class SystemEventTrigger : ITaskTrigger
+ {
+ /// <summary>
+ /// Gets or sets the system event.
+ /// </summary>
+ /// <value>The system event.</value>
+ public SystemEvent SystemEvent { get; set; }
+
+ /// <summary>
+ /// Gets the execution properties of this task.
+ /// </summary>
+ /// <value>
+ /// The execution properties of this task.
+ /// </value>
+ public TaskExecutionOptions TaskOptions { get; set; }
+
+ private readonly ISystemEvents _systemEvents;
+
+ public SystemEventTrigger(ISystemEvents systemEvents)
+ {
+ _systemEvents = systemEvents;
+ }
+
+ /// <summary>
+ /// Stars waiting for the trigger action
+ /// </summary>
+ /// <param name="lastResult">The last result.</param>
+ /// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param>
+ public void Start(TaskResult lastResult, ILogger logger, string taskName, bool isApplicationStartup)
+ {
+ switch (SystemEvent)
+ {
+ case SystemEvent.WakeFromSleep:
+ _systemEvents.Resume += _systemEvents_Resume;
+ break;
+ }
+ }
+
+ private async void _systemEvents_Resume(object sender, EventArgs e)
+ {
+ if (SystemEvent == SystemEvent.WakeFromSleep)
+ {
+ // This value is a bit arbitrary, but add a delay to help ensure network connections have been restored before running the task
+ await Task.Delay(10000).ConfigureAwait(false);
+
+ OnTriggered();
+ }
+ }
+
+ /// <summary>
+ /// Stops waiting for the trigger action
+ /// </summary>
+ public void Stop()
+ {
+ _systemEvents.Resume -= _systemEvents_Resume;
+ }
+
+ /// <summary>
+ /// Occurs when [triggered].
+ /// </summary>
+ public event EventHandler<GenericEventArgs<TaskExecutionOptions>> Triggered;
+
+ /// <summary>
+ /// Called when [triggered].
+ /// </summary>
+ private void OnTriggered()
+ {
+ if (Triggered != null)
+ {
+ Triggered(this, new GenericEventArgs<TaskExecutionOptions>(TaskOptions));
+ }
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Common.Implementations/ScheduledTasks/TaskManager.cs
new file mode 100644
index 0000000000..b0153c5882
--- /dev/null
+++ b/Emby.Common.Implementations/ScheduledTasks/TaskManager.cs
@@ -0,0 +1,334 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Events;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Model.Tasks;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.System;
+
+namespace Emby.Common.Implementations.ScheduledTasks
+{
+ /// <summary>
+ /// Class TaskManager
+ /// </summary>
+ public class TaskManager : ITaskManager
+ {
+ public event EventHandler<GenericEventArgs<IScheduledTaskWorker>> TaskExecuting;
+ public event EventHandler<TaskCompletionEventArgs> TaskCompleted;
+
+ /// <summary>
+ /// Gets the list of Scheduled Tasks
+ /// </summary>
+ /// <value>The scheduled tasks.</value>
+ public IScheduledTaskWorker[] ScheduledTasks { get; private set; }
+
+ /// <summary>
+ /// The _task queue
+ /// </summary>
+ private readonly ConcurrentQueue<Tuple<Type, TaskExecutionOptions>> _taskQueue =
+ new ConcurrentQueue<Tuple<Type, TaskExecutionOptions>>();
+
+ /// <summary>
+ /// Gets or sets the json serializer.
+ /// </summary>
+ /// <value>The json serializer.</value>
+ private IJsonSerializer JsonSerializer { get; set; }
+
+ /// <summary>
+ /// Gets or sets the application paths.
+ /// </summary>
+ /// <value>The application paths.</value>
+ private IApplicationPaths ApplicationPaths { get; set; }
+
+ private readonly ISystemEvents _systemEvents;
+
+ /// <summary>
+ /// Gets the logger.
+ /// </summary>
+ /// <value>The logger.</value>
+ private ILogger Logger { get; set; }
+ private readonly IFileSystem _fileSystem;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TaskManager" /> class.
+ /// </summary>
+ /// <param name="applicationPaths">The application paths.</param>
+ /// <param name="jsonSerializer">The json serializer.</param>
+ /// <param name="logger">The logger.</param>
+ /// <exception cref="System.ArgumentException">kernel</exception>
+ public TaskManager(IApplicationPaths applicationPaths, IJsonSerializer jsonSerializer, ILogger logger, IFileSystem fileSystem, ISystemEvents systemEvents)
+ {
+ ApplicationPaths = applicationPaths;
+ JsonSerializer = jsonSerializer;
+ Logger = logger;
+ _fileSystem = fileSystem;
+ _systemEvents = systemEvents;
+
+ ScheduledTasks = new IScheduledTaskWorker[] { };
+ }
+
+ private void BindToSystemEvent()
+ {
+ _systemEvents.Resume += _systemEvents_Resume;
+ }
+
+ private void _systemEvents_Resume(object sender, EventArgs e)
+ {
+ foreach (var task in ScheduledTasks)
+ {
+ task.ReloadTriggerEvents();
+ }
+ }
+
+ /// <summary>
+ /// Cancels if running and queue.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="options">Task options.</param>
+ public void CancelIfRunningAndQueue<T>(TaskExecutionOptions options)
+ where T : IScheduledTask
+ {
+ var task = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T));
+ ((ScheduledTaskWorker)task).CancelIfRunning();
+
+ QueueScheduledTask<T>(options);
+ }
+
+ public void CancelIfRunningAndQueue<T>()
+ where T : IScheduledTask
+ {
+ CancelIfRunningAndQueue<T>(new TaskExecutionOptions());
+ }
+
+ /// <summary>
+ /// Cancels if running
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ public void CancelIfRunning<T>()
+ where T : IScheduledTask
+ {
+ var task = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T));
+ ((ScheduledTaskWorker)task).CancelIfRunning();
+ }
+
+ /// <summary>
+ /// Queues the scheduled task.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="options">Task options</param>
+ public void QueueScheduledTask<T>(TaskExecutionOptions options)
+ where T : IScheduledTask
+ {
+ var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == typeof(T));
+
+ if (scheduledTask == null)
+ {
+ Logger.Error("Unable to find scheduled task of type {0} in QueueScheduledTask.", typeof(T).Name);
+ }
+ else
+ {
+ QueueScheduledTask(scheduledTask, options);
+ }
+ }
+
+ public void QueueScheduledTask<T>()
+ where T : IScheduledTask
+ {
+ QueueScheduledTask<T>(new TaskExecutionOptions());
+ }
+
+ public void QueueIfNotRunning<T>()
+ where T : IScheduledTask
+ {
+ var task = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T));
+
+ if (task.State != TaskState.Running)
+ {
+ QueueScheduledTask<T>(new TaskExecutionOptions());
+ }
+ }
+
+ public void Execute<T>()
+ where T : IScheduledTask
+ {
+ var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == typeof(T));
+
+ if (scheduledTask == null)
+ {
+ Logger.Error("Unable to find scheduled task of type {0} in Execute.", typeof(T).Name);
+ }
+ else
+ {
+ var type = scheduledTask.ScheduledTask.GetType();
+
+ Logger.Info("Queueing task {0}", type.Name);
+
+ lock (_taskQueue)
+ {
+ if (scheduledTask.State == TaskState.Idle)
+ {
+ Execute(scheduledTask, new TaskExecutionOptions());
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Queues the scheduled task.
+ /// </summary>
+ /// <param name="task">The task.</param>
+ /// <param name="options">The task options.</param>
+ public void QueueScheduledTask(IScheduledTask task, TaskExecutionOptions options)
+ {
+ var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == task.GetType());
+
+ if (scheduledTask == null)
+ {
+ Logger.Error("Unable to find scheduled task of type {0} in QueueScheduledTask.", task.GetType().Name);
+ }
+ else
+ {
+ QueueScheduledTask(scheduledTask, options);
+ }
+ }
+
+ /// <summary>
+ /// Queues the scheduled task.
+ /// </summary>
+ /// <param name="task">The task.</param>
+ /// <param name="options">The task options.</param>
+ private void QueueScheduledTask(IScheduledTaskWorker task, TaskExecutionOptions options)
+ {
+ var type = task.ScheduledTask.GetType();
+
+ Logger.Info("Queueing task {0}", type.Name);
+
+ lock (_taskQueue)
+ {
+ if (task.State == TaskState.Idle)
+ {
+ Execute(task, options);
+ return;
+ }
+
+ _taskQueue.Enqueue(new Tuple<Type, TaskExecutionOptions>(type, options));
+ }
+ }
+
+ /// <summary>
+ /// Adds the tasks.
+ /// </summary>
+ /// <param name="tasks">The tasks.</param>
+ public void AddTasks(IEnumerable<IScheduledTask> tasks)
+ {
+ var myTasks = ScheduledTasks.ToList();
+
+ var list = tasks.ToList();
+ myTasks.AddRange(list.Select(t => new ScheduledTaskWorker(t, ApplicationPaths, this, JsonSerializer, Logger, _fileSystem, _systemEvents)));
+
+ ScheduledTasks = myTasks.ToArray();
+
+ BindToSystemEvent();
+ }
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources.
+ /// </summary>
+ /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool dispose)
+ {
+ foreach (var task in ScheduledTasks)
+ {
+ task.Dispose();
+ }
+ }
+
+ public void Cancel(IScheduledTaskWorker task)
+ {
+ ((ScheduledTaskWorker)task).Cancel();
+ }
+
+ public Task Execute(IScheduledTaskWorker task, TaskExecutionOptions options)
+ {
+ return ((ScheduledTaskWorker)task).Execute(options);
+ }
+
+ /// <summary>
+ /// Called when [task executing].
+ /// </summary>
+ /// <param name="task">The task.</param>
+ internal void OnTaskExecuting(IScheduledTaskWorker task)
+ {
+ EventHelper.FireEventIfNotNull(TaskExecuting, this, new GenericEventArgs<IScheduledTaskWorker>
+ {
+ Argument = task
+
+ }, Logger);
+ }
+
+ /// <summary>
+ /// Called when [task completed].
+ /// </summary>
+ /// <param name="task">The task.</param>
+ /// <param name="result">The result.</param>
+ internal void OnTaskCompleted(IScheduledTaskWorker task, TaskResult result)
+ {
+ EventHelper.FireEventIfNotNull(TaskCompleted, task, new TaskCompletionEventArgs
+ {
+ Result = result,
+ Task = task
+
+ }, Logger);
+
+ ExecuteQueuedTasks();
+ }
+
+ /// <summary>
+ /// Executes the queued tasks.
+ /// </summary>
+ private void ExecuteQueuedTasks()
+ {
+ Logger.Info("ExecuteQueuedTasks");
+
+ // Execute queued tasks
+ lock (_taskQueue)
+ {
+ var list = new List<Tuple<Type, TaskExecutionOptions>>();
+
+ Tuple<Type, TaskExecutionOptions> item;
+ while (_taskQueue.TryDequeue(out item))
+ {
+ if (list.All(i => i.Item1 != item.Item1))
+ {
+ list.Add(item);
+ }
+ }
+
+ foreach (var enqueuedType in list)
+ {
+ var scheduledTask = ScheduledTasks.First(t => t.ScheduledTask.GetType() == enqueuedType.Item1);
+
+ if (scheduledTask.State == TaskState.Idle)
+ {
+ Execute(scheduledTask, enqueuedType.Item2);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/Emby.Common.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
new file mode 100644
index 0000000000..1cad2e9b83
--- /dev/null
+++ b/Emby.Common.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
@@ -0,0 +1,215 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Tasks;
+
+namespace Emby.Common.Implementations.ScheduledTasks.Tasks
+{
+ /// <summary>
+ /// Deletes old cache files
+ /// </summary>
+ public class DeleteCacheFileTask : IScheduledTask, IConfigurableScheduledTask
+ {
+ /// <summary>
+ /// Gets or sets the application paths.
+ /// </summary>
+ /// <value>The application paths.</value>
+ private IApplicationPaths ApplicationPaths { get; set; }
+
+ private readonly ILogger _logger;
+
+ private readonly IFileSystem _fileSystem;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DeleteCacheFileTask" /> class.
+ /// </summary>
+ public DeleteCacheFileTask(IApplicationPaths appPaths, ILogger logger, IFileSystem fileSystem)
+ {
+ ApplicationPaths = appPaths;
+ _logger = logger;
+ _fileSystem = fileSystem;
+ }
+
+ /// <summary>
+ /// Creates the triggers that define when the task will run
+ /// </summary>
+ /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
+ public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ {
+ return new[] {
+
+ // Every so often
+ new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks}
+ };
+ }
+
+ /// <summary>
+ /// Returns the task to be executed
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="progress">The progress.</param>
+ /// <returns>Task.</returns>
+ public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
+ {
+ var minDateModified = DateTime.UtcNow.AddDays(-30);
+
+ try
+ {
+ DeleteCacheFilesFromDirectory(cancellationToken, ApplicationPaths.CachePath, minDateModified, progress);
+ }
+ catch (DirectoryNotFoundException)
+ {
+ // No biggie here. Nothing to delete
+ }
+
+ progress.Report(90);
+
+ minDateModified = DateTime.UtcNow.AddDays(-1);
+
+ try
+ {
+ DeleteCacheFilesFromDirectory(cancellationToken, ApplicationPaths.TempDirectory, minDateModified, progress);
+ }
+ catch (DirectoryNotFoundException)
+ {
+ // No biggie here. Nothing to delete
+ }
+
+ return Task.FromResult(true);
+ }
+
+
+ /// <summary>
+ /// Deletes the cache files from directory with a last write time less than a given date
+ /// </summary>
+ /// <param name="cancellationToken">The task cancellation token.</param>
+ /// <param name="directory">The directory.</param>
+ /// <param name="minDateModified">The min date modified.</param>
+ /// <param name="progress">The progress.</param>
+ private void DeleteCacheFilesFromDirectory(CancellationToken cancellationToken, string directory, DateTime minDateModified, IProgress<double> progress)
+ {
+ var filesToDelete = _fileSystem.GetFiles(directory, true)
+ .Where(f => _fileSystem.GetLastWriteTimeUtc(f) < minDateModified)
+ .ToList();
+
+ var index = 0;
+
+ foreach (var file in filesToDelete)
+ {
+ double percent = index;
+ percent /= filesToDelete.Count;
+
+ progress.Report(100 * percent);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ DeleteFile(file.FullName);
+
+ index++;
+ }
+
+ DeleteEmptyFolders(directory);
+
+ progress.Report(100);
+ }
+
+ private void DeleteEmptyFolders(string parent)
+ {
+ foreach (var directory in _fileSystem.GetDirectoryPaths(parent))
+ {
+ DeleteEmptyFolders(directory);
+ if (!_fileSystem.GetFileSystemEntryPaths(directory).Any())
+ {
+ try
+ {
+ _fileSystem.DeleteDirectory(directory, false);
+ }
+ catch (UnauthorizedAccessException ex)
+ {
+ _logger.ErrorException("Error deleting directory {0}", ex, directory);
+ }
+ catch (IOException ex)
+ {
+ _logger.ErrorException("Error deleting directory {0}", ex, directory);
+ }
+ }
+ }
+ }
+
+ private void DeleteFile(string path)
+ {
+ try
+ {
+ _fileSystem.DeleteFile(path);
+ }
+ catch (UnauthorizedAccessException ex)
+ {
+ _logger.ErrorException("Error deleting file {0}", ex, path);
+ }
+ catch (IOException ex)
+ {
+ _logger.ErrorException("Error deleting file {0}", ex, path);
+ }
+ }
+
+ /// <summary>
+ /// Gets the name of the task
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return "Cache file cleanup"; }
+ }
+
+ public string Key
+ {
+ get { return "DeleteCacheFiles"; }
+ }
+
+ /// <summary>
+ /// Gets the description.
+ /// </summary>
+ /// <value>The description.</value>
+ public string Description
+ {
+ get { return "Deletes cache files no longer needed by the system"; }
+ }
+
+ /// <summary>
+ /// Gets the category.
+ /// </summary>
+ /// <value>The category.</value>
+ public string Category
+ {
+ get
+ {
+ return "Maintenance";
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is hidden.
+ /// </summary>
+ /// <value><c>true</c> if this instance is hidden; otherwise, <c>false</c>.</value>
+ public bool IsHidden
+ {
+ get { return true; }
+ }
+
+ public bool IsEnabled
+ {
+ get { return true; }
+ }
+
+ public bool IsLogged
+ {
+ get { return true; }
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs b/Emby.Common.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
new file mode 100644
index 0000000000..3f43fa8894
--- /dev/null
+++ b/Emby.Common.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
@@ -0,0 +1,138 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Tasks;
+
+namespace Emby.Common.Implementations.ScheduledTasks.Tasks
+{
+ /// <summary>
+ /// Deletes old log files
+ /// </summary>
+ public class DeleteLogFileTask : IScheduledTask, IConfigurableScheduledTask
+ {
+ /// <summary>
+ /// Gets or sets the configuration manager.
+ /// </summary>
+ /// <value>The configuration manager.</value>
+ private IConfigurationManager ConfigurationManager { get; set; }
+
+ private readonly IFileSystem _fileSystem;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DeleteLogFileTask" /> class.
+ /// </summary>
+ /// <param name="configurationManager">The configuration manager.</param>
+ public DeleteLogFileTask(IConfigurationManager configurationManager, IFileSystem fileSystem)
+ {
+ ConfigurationManager = configurationManager;
+ _fileSystem = fileSystem;
+ }
+
+ /// <summary>
+ /// Creates the triggers that define when the task will run
+ /// </summary>
+ /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
+ public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ {
+ return new[] {
+
+ // Every so often
+ new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks}
+ };
+ }
+
+ /// <summary>
+ /// Returns the task to be executed
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="progress">The progress.</param>
+ /// <returns>Task.</returns>
+ public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
+ {
+ // Delete log files more than n days old
+ var minDateModified = DateTime.UtcNow.AddDays(-ConfigurationManager.CommonConfiguration.LogFileRetentionDays);
+
+ var filesToDelete = _fileSystem.GetFiles(ConfigurationManager.CommonApplicationPaths.LogDirectoryPath, true)
+ .Where(f => _fileSystem.GetLastWriteTimeUtc(f) < minDateModified)
+ .ToList();
+
+ var index = 0;
+
+ foreach (var file in filesToDelete)
+ {
+ double percent = index;
+ percent /= filesToDelete.Count;
+
+ progress.Report(100 * percent);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ _fileSystem.DeleteFile(file.FullName);
+
+ index++;
+ }
+
+ progress.Report(100);
+
+ return Task.FromResult(true);
+ }
+
+ public string Key
+ {
+ get { return "CleanLogFiles"; }
+ }
+
+ /// <summary>
+ /// Gets the name of the task
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return "Log file cleanup"; }
+ }
+
+ /// <summary>
+ /// Gets the description.
+ /// </summary>
+ /// <value>The description.</value>
+ public string Description
+ {
+ get { return string.Format("Deletes log files that are more than {0} days old.", ConfigurationManager.CommonConfiguration.LogFileRetentionDays); }
+ }
+
+ /// <summary>
+ /// Gets the category.
+ /// </summary>
+ /// <value>The category.</value>
+ public string Category
+ {
+ get
+ {
+ return "Maintenance";
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is hidden.
+ /// </summary>
+ /// <value><c>true</c> if this instance is hidden; otherwise, <c>false</c>.</value>
+ public bool IsHidden
+ {
+ get { return true; }
+ }
+
+ public bool IsEnabled
+ {
+ get { return true; }
+ }
+
+ public bool IsLogged
+ {
+ get { return true; }
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/ScheduledTasks/Tasks/ReloadLoggerFileTask.cs b/Emby.Common.Implementations/ScheduledTasks/Tasks/ReloadLoggerFileTask.cs
new file mode 100644
index 0000000000..80411de055
--- /dev/null
+++ b/Emby.Common.Implementations/ScheduledTasks/Tasks/ReloadLoggerFileTask.cs
@@ -0,0 +1,112 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Tasks;
+
+namespace Emby.Common.Implementations.ScheduledTasks.Tasks
+{
+ /// <summary>
+ /// Class ReloadLoggerFileTask
+ /// </summary>
+ public class ReloadLoggerFileTask : IScheduledTask, IConfigurableScheduledTask
+ {
+ /// <summary>
+ /// Gets or sets the log manager.
+ /// </summary>
+ /// <value>The log manager.</value>
+ private ILogManager LogManager { get; set; }
+ /// <summary>
+ /// Gets or sets the configuration manager.
+ /// </summary>
+ /// <value>The configuration manager.</value>
+ private IConfigurationManager ConfigurationManager { get; set; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ReloadLoggerFileTask" /> class.
+ /// </summary>
+ /// <param name="logManager">The logManager.</param>
+ /// <param name="configurationManager">The configuration manager.</param>
+ public ReloadLoggerFileTask(ILogManager logManager, IConfigurationManager configurationManager)
+ {
+ LogManager = logManager;
+ ConfigurationManager = configurationManager;
+ }
+
+ /// <summary>
+ /// Gets the default triggers.
+ /// </summary>
+ /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
+ public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ {
+ var trigger = new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerDaily, TimeOfDayTicks = TimeSpan.FromHours(0).Ticks }; //12am
+
+ return new[] { trigger };
+ }
+
+ /// <summary>
+ /// Executes the internal.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="progress">The progress.</param>
+ /// <returns>Task.</returns>
+ public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ progress.Report(0);
+
+ LogManager.ReloadLogger(ConfigurationManager.CommonConfiguration.EnableDebugLevelLogging
+ ? LogSeverity.Debug
+ : LogSeverity.Info);
+
+ return Task.FromResult(true);
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return "Start new log file"; }
+ }
+
+ public string Key { get; }
+
+ /// <summary>
+ /// Gets the description.
+ /// </summary>
+ /// <value>The description.</value>
+ public string Description
+ {
+ get { return "Moves logging to a new file to help reduce log file sizes."; }
+ }
+
+ /// <summary>
+ /// Gets the category.
+ /// </summary>
+ /// <value>The category.</value>
+ public string Category
+ {
+ get { return "Application"; }
+ }
+
+ public bool IsHidden
+ {
+ get { return true; }
+ }
+
+ public bool IsEnabled
+ {
+ get { return true; }
+ }
+
+ public bool IsLogged
+ {
+ get { return true; }
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/ScheduledTasks/WeeklyTrigger.cs b/Emby.Common.Implementations/ScheduledTasks/WeeklyTrigger.cs
new file mode 100644
index 0000000000..91540ba164
--- /dev/null
+++ b/Emby.Common.Implementations/ScheduledTasks/WeeklyTrigger.cs
@@ -0,0 +1,116 @@
+using System;
+using System.Threading;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Tasks;
+
+namespace Emby.Common.Implementations.ScheduledTasks
+{
+ /// <summary>
+ /// Represents a task trigger that fires on a weekly basis
+ /// </summary>
+ public class WeeklyTrigger : ITaskTrigger
+ {
+ /// <summary>
+ /// Get the time of day to trigger the task to run
+ /// </summary>
+ /// <value>The time of day.</value>
+ public TimeSpan TimeOfDay { get; set; }
+
+ /// <summary>
+ /// Gets or sets the day of week.
+ /// </summary>
+ /// <value>The day of week.</value>
+ public DayOfWeek DayOfWeek { get; set; }
+
+ /// <summary>
+ /// Gets the execution properties of this task.
+ /// </summary>
+ /// <value>
+ /// The execution properties of this task.
+ /// </value>
+ public TaskExecutionOptions TaskOptions { get; set; }
+
+ /// <summary>
+ /// Gets or sets the timer.
+ /// </summary>
+ /// <value>The timer.</value>
+ private Timer Timer { get; set; }
+
+ /// <summary>
+ /// Stars waiting for the trigger action
+ /// </summary>
+ /// <param name="lastResult">The last result.</param>
+ /// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param>
+ public void Start(TaskResult lastResult, ILogger logger, string taskName, bool isApplicationStartup)
+ {
+ DisposeTimer();
+
+ var triggerDate = GetNextTriggerDateTime();
+
+ Timer = new Timer(state => OnTriggered(), null, triggerDate - DateTime.Now, TimeSpan.FromMilliseconds(-1));
+ }
+
+ /// <summary>
+ /// Gets the next trigger date time.
+ /// </summary>
+ /// <returns>DateTime.</returns>
+ private DateTime GetNextTriggerDateTime()
+ {
+ var now = DateTime.Now;
+
+ // If it's on the same day
+ if (now.DayOfWeek == DayOfWeek)
+ {
+ // It's either later today, or a week from now
+ return now.TimeOfDay < TimeOfDay ? now.Date.Add(TimeOfDay) : now.Date.AddDays(7).Add(TimeOfDay);
+ }
+
+ var triggerDate = now.Date;
+
+ // Walk the date forward until we get to the trigger day
+ while (triggerDate.DayOfWeek != DayOfWeek)
+ {
+ triggerDate = triggerDate.AddDays(1);
+ }
+
+ // Return the trigger date plus the time offset
+ return triggerDate.Add(TimeOfDay);
+ }
+
+ /// <summary>
+ /// Stops waiting for the trigger action
+ /// </summary>
+ public void Stop()
+ {
+ DisposeTimer();
+ }
+
+ /// <summary>
+ /// Disposes the timer.
+ /// </summary>
+ private void DisposeTimer()
+ {
+ if (Timer != null)
+ {
+ Timer.Dispose();
+ }
+ }
+
+ /// <summary>
+ /// Occurs when [triggered].
+ /// </summary>
+ public event EventHandler<GenericEventArgs<TaskExecutionOptions>> Triggered;
+
+ /// <summary>
+ /// Called when [triggered].
+ /// </summary>
+ private void OnTriggered()
+ {
+ if (Triggered != null)
+ {
+ Triggered(this, new GenericEventArgs<TaskExecutionOptions>(TaskOptions));
+ }
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/Serialization/JsonSerializer.cs b/Emby.Common.Implementations/Serialization/JsonSerializer.cs
new file mode 100644
index 0000000000..c9db336890
--- /dev/null
+++ b/Emby.Common.Implementations/Serialization/JsonSerializer.cs
@@ -0,0 +1,227 @@
+using System;
+using System.IO;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Serialization;
+
+namespace Emby.Common.Implementations.Serialization
+{
+ /// <summary>
+ /// Provides a wrapper around third party json serialization.
+ /// </summary>
+ public class JsonSerializer : IJsonSerializer
+ {
+ private readonly IFileSystem _fileSystem;
+ private readonly ILogger _logger;
+
+ public JsonSerializer(IFileSystem fileSystem, ILogger logger)
+ {
+ _fileSystem = fileSystem;
+ _logger = logger;
+ Configure();
+ }
+
+ /// <summary>
+ /// Serializes to stream.
+ /// </summary>
+ /// <param name="obj">The obj.</param>
+ /// <param name="stream">The stream.</param>
+ /// <exception cref="System.ArgumentNullException">obj</exception>
+ public void SerializeToStream(object obj, Stream stream)
+ {
+ if (obj == null)
+ {
+ throw new ArgumentNullException("obj");
+ }
+
+ if (stream == null)
+ {
+ throw new ArgumentNullException("stream");
+ }
+
+ ServiceStack.Text.JsonSerializer.SerializeToStream(obj, obj.GetType(), stream);
+ }
+
+ /// <summary>
+ /// Serializes to file.
+ /// </summary>
+ /// <param name="obj">The obj.</param>
+ /// <param name="file">The file.</param>
+ /// <exception cref="System.ArgumentNullException">obj</exception>
+ public void SerializeToFile(object obj, string file)
+ {
+ if (obj == null)
+ {
+ throw new ArgumentNullException("obj");
+ }
+
+ if (string.IsNullOrEmpty(file))
+ {
+ throw new ArgumentNullException("file");
+ }
+
+ using (Stream stream = _fileSystem.GetFileStream(file, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
+ {
+ SerializeToStream(obj, stream);
+ }
+ }
+
+ private Stream OpenFile(string path)
+ {
+ _logger.Debug("Deserializing file {0}", path);
+ return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 131072);
+ }
+
+ /// <summary>
+ /// Deserializes from file.
+ /// </summary>
+ /// <param name="type">The type.</param>
+ /// <param name="file">The file.</param>
+ /// <returns>System.Object.</returns>
+ /// <exception cref="System.ArgumentNullException">type</exception>
+ public object DeserializeFromFile(Type type, string file)
+ {
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ if (string.IsNullOrEmpty(file))
+ {
+ throw new ArgumentNullException("file");
+ }
+
+ using (Stream stream = OpenFile(file))
+ {
+ return DeserializeFromStream(stream, type);
+ }
+ }
+
+ /// <summary>
+ /// Deserializes from file.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="file">The file.</param>
+ /// <returns>``0.</returns>
+ /// <exception cref="System.ArgumentNullException">file</exception>
+ public T DeserializeFromFile<T>(string file)
+ where T : class
+ {
+ if (string.IsNullOrEmpty(file))
+ {
+ throw new ArgumentNullException("file");
+ }
+
+ using (Stream stream = OpenFile(file))
+ {
+ return DeserializeFromStream<T>(stream);
+ }
+ }
+
+ /// <summary>
+ /// Deserializes from stream.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="stream">The stream.</param>
+ /// <returns>``0.</returns>
+ /// <exception cref="System.ArgumentNullException">stream</exception>
+ public T DeserializeFromStream<T>(Stream stream)
+ {
+ if (stream == null)
+ {
+ throw new ArgumentNullException("stream");
+ }
+
+ return ServiceStack.Text.JsonSerializer.DeserializeFromStream<T>(stream);
+ }
+
+ /// <summary>
+ /// Deserializes from string.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="text">The text.</param>
+ /// <returns>``0.</returns>
+ /// <exception cref="System.ArgumentNullException">text</exception>
+ public T DeserializeFromString<T>(string text)
+ {
+ if (string.IsNullOrEmpty(text))
+ {
+ throw new ArgumentNullException("text");
+ }
+
+ return ServiceStack.Text.JsonSerializer.DeserializeFromString<T>(text);
+ }
+
+ /// <summary>
+ /// Deserializes from stream.
+ /// </summary>
+ /// <param name="stream">The stream.</param>
+ /// <param name="type">The type.</param>
+ /// <returns>System.Object.</returns>
+ /// <exception cref="System.ArgumentNullException">stream</exception>
+ public object DeserializeFromStream(Stream stream, Type type)
+ {
+ if (stream == null)
+ {
+ throw new ArgumentNullException("stream");
+ }
+
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ return ServiceStack.Text.JsonSerializer.DeserializeFromStream(type, stream);
+ }
+
+ /// <summary>
+ /// Configures this instance.
+ /// </summary>
+ private void Configure()
+ {
+ ServiceStack.Text.JsConfig.DateHandler = ServiceStack.Text.DateHandler.ISO8601;
+ ServiceStack.Text.JsConfig.ExcludeTypeInfo = true;
+ ServiceStack.Text.JsConfig.IncludeNullValues = false;
+ ServiceStack.Text.JsConfig.AlwaysUseUtc = true;
+ ServiceStack.Text.JsConfig.AssumeUtc = true;
+ }
+
+ /// <summary>
+ /// Deserializes from string.
+ /// </summary>
+ /// <param name="json">The json.</param>
+ /// <param name="type">The type.</param>
+ /// <returns>System.Object.</returns>
+ /// <exception cref="System.ArgumentNullException">json</exception>
+ public object DeserializeFromString(string json, Type type)
+ {
+ if (string.IsNullOrEmpty(json))
+ {
+ throw new ArgumentNullException("json");
+ }
+
+ if (type == null)
+ {
+ throw new ArgumentNullException("type");
+ }
+
+ return ServiceStack.Text.JsonSerializer.DeserializeFromString(json, type);
+ }
+
+ /// <summary>
+ /// Serializes to string.
+ /// </summary>
+ /// <param name="obj">The obj.</param>
+ /// <returns>System.String.</returns>
+ /// <exception cref="System.ArgumentNullException">obj</exception>
+ public string SerializeToString(object obj)
+ {
+ if (obj == null)
+ {
+ throw new ArgumentNullException("obj");
+ }
+
+ return ServiceStack.Text.JsonSerializer.SerializeToString(obj, obj.GetType());
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/Serialization/XmlSerializer.cs b/Emby.Common.Implementations/Serialization/XmlSerializer.cs
new file mode 100644
index 0000000000..3583f998e5
--- /dev/null
+++ b/Emby.Common.Implementations/Serialization/XmlSerializer.cs
@@ -0,0 +1,138 @@
+using MediaBrowser.Model.Serialization;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml;
+using System.Xml.Serialization;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+
+namespace Emby.Common.Implementations.Serialization
+{
+ /// <summary>
+ /// Provides a wrapper around third party xml serialization.
+ /// </summary>
+ public class MyXmlSerializer : IXmlSerializer
+ {
+ private readonly IFileSystem _fileSystem;
+ private readonly ILogger _logger;
+
+ public MyXmlSerializer(IFileSystem fileSystem, ILogger logger)
+ {
+ _fileSystem = fileSystem;
+ _logger = logger;
+ }
+
+ // Need to cache these
+ // http://dotnetcodebox.blogspot.com/2013/01/xmlserializer-class-may-result-in.html
+ private readonly Dictionary<string, XmlSerializer> _serializers =
+ new Dictionary<string, XmlSerializer>();
+
+ private XmlSerializer GetSerializer(Type type)
+ {
+ var key = type.FullName;
+ lock (_serializers)
+ {
+ XmlSerializer serializer;
+ if (!_serializers.TryGetValue(key, out serializer))
+ {
+ serializer = new XmlSerializer(type);
+ _serializers[key] = serializer;
+ }
+ return serializer;
+ }
+ }
+
+ /// <summary>
+ /// Serializes to writer.
+ /// </summary>
+ /// <param name="obj">The obj.</param>
+ /// <param name="writer">The writer.</param>
+ private void SerializeToWriter(object obj, XmlWriter writer)
+ {
+ var netSerializer = GetSerializer(obj.GetType());
+ netSerializer.Serialize(writer, obj);
+ }
+
+ /// <summary>
+ /// Deserializes from stream.
+ /// </summary>
+ /// <param name="type">The type.</param>
+ /// <param name="stream">The stream.</param>
+ /// <returns>System.Object.</returns>
+ public object DeserializeFromStream(Type type, Stream stream)
+ {
+ using (var reader = XmlReader.Create(stream))
+ {
+ var netSerializer = GetSerializer(type);
+ return netSerializer.Deserialize(reader);
+ }
+ }
+
+ /// <summary>
+ /// Serializes to stream.
+ /// </summary>
+ /// <param name="obj">The obj.</param>
+ /// <param name="stream">The stream.</param>
+ public void SerializeToStream(object obj, Stream stream)
+ {
+#if NET46
+ using (var writer = new XmlTextWriter(stream, null))
+ {
+ writer.Formatting = Formatting.Indented;
+ SerializeToWriter(obj, writer);
+ }
+#else
+ using (var writer = XmlWriter.Create(stream))
+ {
+ SerializeToWriter(obj, writer);
+ }
+#endif
+ }
+
+ /// <summary>
+ /// Serializes to file.
+ /// </summary>
+ /// <param name="obj">The obj.</param>
+ /// <param name="file">The file.</param>
+ public void SerializeToFile(object obj, string file)
+ {
+ _logger.Debug("Serializing to file {0}", file);
+ using (var stream = new FileStream(file, FileMode.Create))
+ {
+ SerializeToStream(obj, stream);
+ }
+ }
+
+ /// <summary>
+ /// Deserializes from file.
+ /// </summary>
+ /// <param name="type">The type.</param>
+ /// <param name="file">The file.</param>
+ /// <returns>System.Object.</returns>
+ public object DeserializeFromFile(Type type, string file)
+ {
+ _logger.Debug("Deserializing file {0}", file);
+ using (var stream = _fileSystem.OpenRead(file))
+ {
+ return DeserializeFromStream(type, stream);
+ }
+ }
+
+ /// <summary>
+ /// Deserializes from bytes.
+ /// </summary>
+ /// <param name="type">The type.</param>
+ /// <param name="buffer">The buffer.</param>
+ /// <returns>System.Object.</returns>
+ public object DeserializeFromBytes(Type type, byte[] buffer)
+ {
+ using (var stream = new MemoryStream(buffer))
+ {
+ return DeserializeFromStream(type, stream);
+ }
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/TextEncoding/TextEncoding.cs b/Emby.Common.Implementations/TextEncoding/TextEncoding.cs
new file mode 100644
index 0000000000..254d352224
--- /dev/null
+++ b/Emby.Common.Implementations/TextEncoding/TextEncoding.cs
@@ -0,0 +1,43 @@
+using System.Text;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Text;
+
+namespace Emby.Common.Implementations.TextEncoding
+{
+ public class TextEncoding : ITextEncoding
+ {
+ private readonly IFileSystem _fileSystem;
+
+ public TextEncoding(IFileSystem fileSystem)
+ {
+ _fileSystem = fileSystem;
+ }
+
+ public Encoding GetASCIIEncoding()
+ {
+ return Encoding.ASCII;
+ }
+
+ public Encoding GetFileEncoding(string srcFile)
+ {
+ // *** Detect byte order mark if any - otherwise assume default
+ var buffer = new byte[5];
+
+ using (var file = _fileSystem.OpenRead(srcFile))
+ {
+ file.Read(buffer, 0, 5);
+ }
+
+ if (buffer[0] == 0xef && buffer[1] == 0xbb && buffer[2] == 0xbf)
+ return Encoding.UTF8;
+ if (buffer[0] == 0xfe && buffer[1] == 0xff)
+ return Encoding.Unicode;
+ if (buffer[0] == 0 && buffer[1] == 0 && buffer[2] == 0xfe && buffer[3] == 0xff)
+ return Encoding.UTF32;
+ if (buffer[0] == 0x2b && buffer[1] == 0x2f && buffer[2] == 0x76)
+ return Encoding.UTF7;
+
+ return null;
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/Threading/CommonTimer.cs b/Emby.Common.Implementations/Threading/CommonTimer.cs
new file mode 100644
index 0000000000..8895f6798a
--- /dev/null
+++ b/Emby.Common.Implementations/Threading/CommonTimer.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Threading;
+
+namespace Emby.Common.Implementations.Threading
+{
+ public class CommonTimer : ITimer
+ {
+ private readonly Timer _timer;
+
+ public CommonTimer(Action<object> callback, object state, TimeSpan dueTime, TimeSpan period)
+ {
+ _timer = new Timer(new TimerCallback(callback), state, dueTime, period);
+ }
+
+ public CommonTimer(Action<object> callback, object state, int dueTimeMs, int periodMs)
+ {
+ _timer = new Timer(new TimerCallback(callback), state, dueTimeMs, periodMs);
+ }
+
+ public void Change(TimeSpan dueTime, TimeSpan period)
+ {
+ _timer.Change(dueTime, period);
+ }
+
+ public void Change(int dueTimeMs, int periodMs)
+ {
+ _timer.Change(dueTimeMs, periodMs);
+ }
+
+ public void Dispose()
+ {
+ _timer.Dispose();
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/Threading/TimerFactory.cs b/Emby.Common.Implementations/Threading/TimerFactory.cs
new file mode 100644
index 0000000000..028dd09639
--- /dev/null
+++ b/Emby.Common.Implementations/Threading/TimerFactory.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Threading;
+
+namespace Emby.Common.Implementations.Threading
+{
+ public class TimerFactory : ITimerFactory
+ {
+ public ITimer Create(Action<object> callback, object state, TimeSpan dueTime, TimeSpan period)
+ {
+ return new CommonTimer(callback, state, dueTime, period);
+ }
+
+ public ITimer Create(Action<object> callback, object state, int dueTimeMs, int periodMs)
+ {
+ return new CommonTimer(callback, state, dueTimeMs, periodMs);
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/Xml/XmlReaderSettingsFactory.cs b/Emby.Common.Implementations/Xml/XmlReaderSettingsFactory.cs
new file mode 100644
index 0000000000..806290cf44
--- /dev/null
+++ b/Emby.Common.Implementations/Xml/XmlReaderSettingsFactory.cs
@@ -0,0 +1,22 @@
+using System.Xml;
+using MediaBrowser.Model.Xml;
+
+namespace Emby.Common.Implementations.Xml
+{
+ public class XmlReaderSettingsFactory : IXmlReaderSettingsFactory
+ {
+ public XmlReaderSettings Create(bool enableValidation)
+ {
+ var settings = new XmlReaderSettings();
+
+ if (!enableValidation)
+ {
+#if NET46
+ settings.ValidationType = ValidationType.None;
+#endif
+ }
+
+ return settings;
+ }
+ }
+}
diff --git a/Emby.Common.Implementations/project.json b/Emby.Common.Implementations/project.json
new file mode 100644
index 0000000000..674101e8a7
--- /dev/null
+++ b/Emby.Common.Implementations/project.json
@@ -0,0 +1,71 @@
+{
+ "version": "1.0.0-*",
+
+ "dependencies": {
+
+ },
+
+ "frameworks": {
+ "net46": {
+ "frameworkAssemblies": {
+ "System.Collections": "4.0.0.0",
+ "System.IO": "4.0.0.0",
+ "System.Net": "4.0.0.0",
+ "System.Net.Http": "4.0.0.0",
+ "System.Net.Primitives": "4.0.0.0",
+ "System.Net.Http.WebRequest": "4.0.0.0",
+ "System.Reflection": "4.0.0.0",
+ "System.Runtime": "4.0.0.0",
+ "System.Runtime.Extensions": "4.0.0.0",
+ "System.Text.Encoding": "4.0.0.0",
+ "System.Threading": "4.0.0.0",
+ "System.Threading.Tasks": "4.0.0.0",
+ "System.Xml.ReaderWriter": "4.0.0"
+ },
+ "dependencies": {
+ "SimpleInjector": "3.2.4",
+ "ServiceStack.Text": "4.5.4",
+ "NLog": "4.4.0-betaV15",
+ "sharpcompress": "0.14.0",
+ "MediaBrowser.Model": {
+ "target": "project"
+ },
+ "MediaBrowser.Common": {
+ "target": "project"
+ }
+ }
+ },
+ "netstandard1.6": {
+ "imports": "dnxcore50",
+ "dependencies": {
+ "NETStandard.Library": "1.6.1",
+ "System.IO.FileSystem.DriveInfo": "4.3.0",
+ "System.Diagnostics.Process": "4.3.0",
+ "System.Threading.Timer": "4.3.0",
+ "System.Net.Requests": "4.3.0",
+ "System.Xml.ReaderWriter": "4.3.0",
+ "System.Xml.XmlSerializer": "4.3.0",
+ "System.Net.Http": "4.3.0",
+ "System.Net.Primitives": "4.3.0",
+ "System.Net.Sockets": "4.3.0",
+ "System.Net.NetworkInformation": "4.3.0",
+ "System.Net.NameResolution": "4.3.0",
+ "System.Runtime.InteropServices.RuntimeInformation": "4.3.0",
+ "System.Reflection": "4.3.0",
+ "System.Reflection.Primitives": "4.3.0",
+ "System.Runtime.Loader": "4.3.0",
+ "SimpleInjector": "3.2.4",
+ "ServiceStack.Text.Core": "1.0.27",
+ "NLog": "4.4.0-betaV15",
+ "sharpcompress": "0.14.0",
+ "System.AppDomain": "2.0.11",
+ "MediaBrowser.Model": {
+ "target": "project"
+ },
+ "MediaBrowser.Common": {
+ "target": "project"
+ }
+ }
+ }
+ }
+}