diff options
| author | Luke <luke.pulverenti@gmail.com> | 2016-12-18 00:44:33 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2016-12-18 00:44:33 -0500 |
| commit | e7cebb91a73354dc3e0d0b6340c9fbd6511f4406 (patch) | |
| tree | 6f1c368c766c17b7514fe749c0e92e69cd89194a /Emby.Common.Implementations | |
| parent | 025905a3e4d50b9a2e07fbf4ff0a203af6604ced (diff) | |
| parent | aaa027f3229073e9a40756c3157d41af2a442922 (diff) | |
Merge pull request #2350 from MediaBrowser/beta
Beta
Diffstat (limited to 'Emby.Common.Implementations')
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" + } + } + } + } +} |
