Explorar o código

#62 - File locking problem in cache

Luke Pulverenti %!s(int64=12) %!d(string=hai) anos
pai
achega
fa884f3fea

+ 2 - 2
MediaBrowser.Api/Playback/Progressive/AudioService.cs

@@ -15,7 +15,7 @@ namespace MediaBrowser.Api.Playback.Progressive
     [Route("/Audio/{Id}/stream.flac", "GET")]
     [Route("/Audio/{Id}/stream.ogg", "GET")]
     [Route("/Audio/{Id}/stream.oga", "GET")]
-    [Route("/Audio/{Id}/stream.webma", "GET")]
+    [Route("/Audio/{Id}/stream.webm", "GET")]
     [Route("/Audio/{Id}/stream", "GET")]
     [Route("/Audio/{Id}/stream.mp3", "HEAD")]
     [Route("/Audio/{Id}/stream.wma", "HEAD")]
@@ -23,7 +23,7 @@ namespace MediaBrowser.Api.Playback.Progressive
     [Route("/Audio/{Id}/stream.flac", "HEAD")]
     [Route("/Audio/{Id}/stream.ogg", "HEAD")]
     [Route("/Audio/{Id}/stream.oga", "HEAD")]
-    [Route("/Audio/{Id}/stream.webma", "HEAD")]
+    [Route("/Audio/{Id}/stream.webm", "HEAD")]
     [Route("/Audio/{Id}/stream", "HEAD")]
     [Api(Description = "Gets an audio stream")]
     public class GetAudioStream : StreamRequest

+ 4 - 6
MediaBrowser.Api/UserService.cs

@@ -167,11 +167,9 @@ namespace MediaBrowser.Api
         {
             var dtoBuilder = new DtoBuilder(Logger, _libraryManager, _userManager);
 
-            var tasks = _userManager.Users.OrderBy(u => u.Name).Select(dtoBuilder.GetUserDto).ToArray();
+            var users = _userManager.Users.OrderBy(u => u.Name).Select(dtoBuilder.GetUserDto).ToArray();
 
-            var task = Task.WhenAll(tasks);
-
-            return ToOptimizedResult(task.Result);
+            return ToOptimizedResult(users);
         }
 
         /// <summary>
@@ -188,7 +186,7 @@ namespace MediaBrowser.Api
                 throw new ResourceNotFoundException("User not found");
             }
 
-            var result = new DtoBuilder(Logger, _libraryManager, _userManager).GetUserDto(user).Result;
+            var result = new DtoBuilder(Logger, _libraryManager, _userManager).GetUserDto(user);
 
             return ToOptimizedResult(result);
         }
@@ -302,7 +300,7 @@ namespace MediaBrowser.Api
 
             newUser.UpdateConfiguration(dtoUser.Configuration, _xmlSerializer);
 
-            var result = new DtoBuilder(Logger, _libraryManager, _userManager).GetUserDto(newUser).Result;
+            var result = new DtoBuilder(Logger, _libraryManager, _userManager).GetUserDto(newUser);
 
             return ToOptimizedResult(result);
         }

+ 0 - 97
MediaBrowser.Common/Extensions/NamedLock.cs

@@ -1,97 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Common.Extensions
-{
-    /// <summary>
-    /// Class NamedLock
-    /// </summary>
-    public class NamedLock : IDisposable
-    {
-        /// <summary>
-        /// The _locks
-        /// </summary>
-        private readonly Dictionary<string, SemaphoreSlim> _locks = new Dictionary<string, SemaphoreSlim>();
-
-        /// <summary>
-        /// Waits the async.
-        /// </summary>
-        /// <param name="name">The name.</param>
-        /// <returns>Task.</returns>
-        public Task WaitAsync(string name)
-        {
-            return GetLock(name).WaitAsync();
-        }
-
-        /// <summary>
-        /// Releases the specified name.
-        /// </summary>
-        /// <param name="name">The name.</param>
-        public void Release(string name)
-        {
-            SemaphoreSlim semaphore;
-
-            if (_locks.TryGetValue(name, out semaphore))
-            {
-                semaphore.Release();
-            }
-        }
-
-        /// <summary>
-        /// Gets the lock.
-        /// </summary>
-        /// <param name="filename">The filename.</param>
-        /// <returns>System.Object.</returns>
-        private SemaphoreSlim GetLock(string filename)
-        {
-            SemaphoreSlim fileLock;
-            lock (_locks)
-            {
-                if (!_locks.TryGetValue(filename, out fileLock))
-                {
-                    fileLock = new SemaphoreSlim(1,1);
-                    _locks[filename] = fileLock;
-                }
-            }
-            return fileLock;
-        }
-
-        /// <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)
-            {
-                DisposeLocks();
-            }
-        }
-
-        /// <summary>
-        /// Disposes the locks.
-        /// </summary>
-        private void DisposeLocks()
-        {
-            lock (_locks)
-            {
-                foreach (var semaphore in _locks.Values)
-                {
-                    semaphore.Dispose();
-                }
-
-                _locks.Clear();
-            }
-        }
-    }
-}

+ 0 - 25
MediaBrowser.Common/IO/FileSystemRepository.cs

@@ -2,7 +2,6 @@
 using System;
 using System.Collections.Concurrent;
 using System.IO;
-using System.Threading.Tasks;
 
 namespace MediaBrowser.Common.IO
 {
@@ -18,11 +17,6 @@ namespace MediaBrowser.Common.IO
         /// </summary>
         private readonly ConcurrentDictionary<string, string> _subFolderPaths = new ConcurrentDictionary<string, string>();
 
-        /// <summary>
-        /// The _file locks
-        /// </summary>
-        private readonly NamedLock _fileLocks = new NamedLock();
-
         /// <summary>
         /// Gets or sets the path.
         /// </summary>
@@ -170,24 +164,6 @@ namespace MediaBrowser.Common.IO
             return File.Exists(path);
         }
 
-        /// <summary>
-        /// Waits for lock.
-        /// </summary>
-        /// <param name="resourcePath">The resource path.</param>
-        public Task WaitForLockAsync(string resourcePath)
-        {
-            return _fileLocks.WaitAsync(resourcePath);
-        }
-
-        /// <summary>
-        /// Releases the lock.
-        /// </summary>
-        /// <param name="resourcePath">The resource path.</param>
-        public void ReleaseLock(string resourcePath)
-        {
-            _fileLocks.Release(resourcePath);
-        }
-
         /// <summary>
         /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
         /// </summary>
@@ -204,7 +180,6 @@ namespace MediaBrowser.Common.IO
         {
             if (dispose)
             {
-                _fileLocks.Dispose();
             }
         }
     }

+ 0 - 1
MediaBrowser.Common/MediaBrowser.Common.csproj

@@ -61,7 +61,6 @@
     <Compile Include="Events\EventHelper.cs" />
     <Compile Include="Extensions\BaseExtensions.cs" />
     <Compile Include="Events\GenericEventArgs.cs" />
-    <Compile Include="Extensions\NamedLock.cs" />
     <Compile Include="Extensions\ResourceNotFoundException.cs" />
     <Compile Include="IO\FileSystemRepository.cs" />
     <Compile Include="IO\IIsoManager.cs" />

+ 161 - 100
MediaBrowser.Controller/Drawing/ImageManager.cs

@@ -15,6 +15,7 @@ using System.Drawing.Drawing2D;
 using System.Drawing.Imaging;
 using System.IO;
 using System.Linq;
+using System.Threading;
 using System.Threading.Tasks;
 
 namespace MediaBrowser.Controller.Drawing
@@ -50,7 +51,7 @@ namespace MediaBrowser.Controller.Drawing
         /// <summary>
         /// The cached imaged sizes
         /// </summary>
-        private readonly ConcurrentDictionary<string, Task<ImageSize>> _cachedImagedSizes = new ConcurrentDictionary<string, Task<ImageSize>>();
+        private readonly ConcurrentDictionary<string, ImageSize> _cachedImagedSizes = new ConcurrentDictionary<string, ImageSize>();
 
         /// <summary>
         /// The _logger
@@ -67,12 +68,18 @@ namespace MediaBrowser.Controller.Drawing
         /// </summary>
         private readonly Kernel _kernel;
 
+        /// <summary>
+        /// The _locks
+        /// </summary>
+        private readonly ConcurrentDictionary<string, SemaphoreSlim> _locks = new ConcurrentDictionary<string, SemaphoreSlim>();
+
         /// <summary>
         /// Initializes a new instance of the <see cref="ImageManager" /> class.
         /// </summary>
         /// <param name="kernel">The kernel.</param>
         /// <param name="protobufSerializer">The protobuf serializer.</param>
         /// <param name="logger">The logger.</param>
+        /// <param name="appPaths">The app paths.</param>
         public ImageManager(Kernel kernel, IProtobufSerializer protobufSerializer, ILogger logger, IServerApplicationPaths appPaths)
         {
             _protobufSerializer = protobufSerializer;
@@ -117,15 +124,7 @@ namespace MediaBrowser.Controller.Drawing
 
             if (cropWhitespace)
             {
-                try
-                {
-                    originalImagePath = await GetCroppedImage(originalImagePath, dateModified).ConfigureAwait(false);
-                }
-                catch (Exception ex)
-                {
-                    // We have to have a catch-all here because some of the .net image methods throw a plain old Exception
-                    _logger.ErrorException("Error cropping image", ex);
-                }
+                originalImagePath = await GetCroppedImage(originalImagePath, dateModified).ConfigureAwait(false);
             }
 
             try
@@ -140,12 +139,12 @@ namespace MediaBrowser.Controller.Drawing
                     originalImagePath = ehnancedImagePath;
                 }
             }
-            catch
+            catch (Exception ex)
             {
-                _logger.Error("Error enhancing image");
+                _logger.Error("Error enhancing image", ex);
             }
 
-            var originalImageSize = await GetImageSize(originalImagePath, dateModified).ConfigureAwait(false);
+            var originalImageSize = GetImageSize(originalImagePath, dateModified);
 
             // Determine the output size based on incoming parameters
             var newSize = DrawingUtils.Resize(originalImageSize, width, height, maxWidth, maxHeight);
@@ -158,67 +157,102 @@ namespace MediaBrowser.Controller.Drawing
             var cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality.Value, dateModified);
 
             // Grab the cache file if it already exists
-            try
+            if (File.Exists(cacheFilePath))
             {
                 using (var fileStream = new FileStream(cacheFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous))
                 {
                     await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
+                    return;
                 }
-                return;
             }
-            catch (FileNotFoundException)
+
+            var semaphore = GetLock(cacheFilePath);
+
+            await semaphore.WaitAsync().ConfigureAwait(false);
+
+            // Check again in case of lock contention
+            if (File.Exists(cacheFilePath))
             {
-                // Cache file doesn't exist. No biggie.
+                try
+                {
+                    using (var fileStream = new FileStream(cacheFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous))
+                    {
+                        await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
+                        return;
+                    }
+                }
+                finally
+                {
+                    semaphore.Release();
+                }
             }
 
-            using (var fileStream = File.OpenRead(originalImagePath))
+            try
             {
-                using (var originalImage = Bitmap.FromStream(fileStream, true, false))
+                using (var fileStream = File.OpenRead(originalImagePath))
                 {
-                    var newWidth = Convert.ToInt32(newSize.Width);
-                    var newHeight = Convert.ToInt32(newSize.Height);
+                    using (var originalImage = Image.FromStream(fileStream, true, false))
+                    {
+                        var newWidth = Convert.ToInt32(newSize.Width);
+                        var newHeight = Convert.ToInt32(newSize.Height);
 
-                    // Graphics.FromImage will throw an exception if the PixelFormat is Indexed, so we need to handle that here
-                    var thumbnail = !ImageExtensions.IsPixelFormatSupportedByGraphicsObject(originalImage.PixelFormat) ? new Bitmap(originalImage, newWidth, newHeight) : new Bitmap(newWidth, newHeight, originalImage.PixelFormat);
+                        // Graphics.FromImage will throw an exception if the PixelFormat is Indexed, so we need to handle that here
+                        var thumbnail = !ImageExtensions.IsPixelFormatSupportedByGraphicsObject(originalImage.PixelFormat) ? new Bitmap(originalImage, newWidth, newHeight) : new Bitmap(newWidth, newHeight, originalImage.PixelFormat);
 
-                    // Preserve the original resolution
-                    thumbnail.SetResolution(originalImage.HorizontalResolution, originalImage.VerticalResolution);
+                        // Preserve the original resolution
+                        thumbnail.SetResolution(originalImage.HorizontalResolution, originalImage.VerticalResolution);
 
-                    var thumbnailGraph = Graphics.FromImage(thumbnail);
+                        var thumbnailGraph = Graphics.FromImage(thumbnail);
 
-                    thumbnailGraph.CompositingQuality = CompositingQuality.HighQuality;
-                    thumbnailGraph.SmoothingMode = SmoothingMode.HighQuality;
-                    thumbnailGraph.InterpolationMode = InterpolationMode.HighQualityBicubic;
-                    thumbnailGraph.PixelOffsetMode = PixelOffsetMode.HighQuality;
-                    thumbnailGraph.CompositingMode = CompositingMode.SourceOver;
+                        thumbnailGraph.CompositingQuality = CompositingQuality.HighQuality;
+                        thumbnailGraph.SmoothingMode = SmoothingMode.HighQuality;
+                        thumbnailGraph.InterpolationMode = InterpolationMode.HighQualityBicubic;
+                        thumbnailGraph.PixelOffsetMode = PixelOffsetMode.HighQuality;
+                        thumbnailGraph.CompositingMode = CompositingMode.SourceOver;
 
-                    thumbnailGraph.DrawImage(originalImage, 0, 0, newWidth, newHeight);
+                        thumbnailGraph.DrawImage(originalImage, 0, 0, newWidth, newHeight);
 
-                    var outputFormat = originalImage.RawFormat;
+                        var outputFormat = originalImage.RawFormat;
 
-                    using (var memoryStream = new MemoryStream { })
-                    {
-                        // Save to the memory stream
-                        thumbnail.Save(outputFormat, memoryStream, quality.Value);
+                        using (var memoryStream = new MemoryStream { })
+                        {
+                            // Save to the memory stream
+                            thumbnail.Save(outputFormat, memoryStream, quality.Value);
 
-                        var bytes = memoryStream.ToArray();
+                            var bytes = memoryStream.ToArray();
 
-                        var outputTask = Task.Run(async () => await toStream.WriteAsync(bytes, 0, bytes.Length));
+                            var outputTask = toStream.WriteAsync(bytes, 0, bytes.Length);
 
-                        // Save to the cache location
-                        using (var cacheFileStream = new FileStream(cacheFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous))
-                        {
-                            // Save to the filestream
-                            await cacheFileStream.WriteAsync(bytes, 0, bytes.Length);
+                            // kick off a task to cache the result
+                            Task.Run(() => CacheResizedImage(cacheFilePath, bytes));
+
+                            await outputTask.ConfigureAwait(false);
                         }
 
-                        await outputTask.ConfigureAwait(false);
+                        thumbnailGraph.Dispose();
+                        thumbnail.Dispose();
                     }
-
-                    thumbnailGraph.Dispose();
-                    thumbnail.Dispose();
                 }
             }
+            finally
+            {
+                semaphore.Release();
+            }
+        }
+
+        /// <summary>
+        /// Caches the resized image.
+        /// </summary>
+        /// <param name="cacheFilePath">The cache file path.</param>
+        /// <param name="bytes">The bytes.</param>
+        private async void CacheResizedImage(string cacheFilePath, byte[] bytes)
+        {
+            // Save to the cache location
+            using (var cacheFileStream = new FileStream(cacheFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous))
+            {
+                // Save to the filestream
+                await cacheFileStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
+            }
         }
 
         /// <summary>
@@ -252,7 +286,7 @@ namespace MediaBrowser.Controller.Drawing
         /// <param name="dateModified">The date modified.</param>
         /// <returns>Task{ImageSize}.</returns>
         /// <exception cref="System.ArgumentNullException">imagePath</exception>
-        public Task<ImageSize> GetImageSize(string imagePath, DateTime dateModified)
+        public ImageSize GetImageSize(string imagePath, DateTime dateModified)
         {
             if (string.IsNullOrEmpty(imagePath))
             {
@@ -261,18 +295,7 @@ namespace MediaBrowser.Controller.Drawing
 
             var name = imagePath + "datemodified=" + dateModified.Ticks;
 
-            return _cachedImagedSizes.GetOrAdd(name, keyName => GetImageSizeTask(keyName, imagePath));
-        }
-
-        /// <summary>
-        /// Gets cached image dimensions, or results null if non-existant
-        /// </summary>
-        /// <param name="keyName">Name of the key.</param>
-        /// <param name="imagePath">The image path.</param>
-        /// <returns>Task{ImageSize}.</returns>
-        private Task<ImageSize> GetImageSizeTask(string keyName, string imagePath)
-        {
-            return Task.Run(() => GetImageSize(keyName, imagePath));
+            return _cachedImagedSizes.GetOrAdd(name, keyName => GetImageSize(keyName, imagePath));
         }
 
         /// <summary>
@@ -297,27 +320,14 @@ namespace MediaBrowser.Controller.Drawing
                 // Cache file doesn't exist no biggie
             }
 
-            var size = ImageHeader.GetDimensions(imagePath, _logger);
+            _logger.Debug("Getting image size for {0}", imagePath);
 
-            var imageSize = new ImageSize { Width = size.Width, Height = size.Height };
+            var size = ImageHeader.GetDimensions(imagePath, _logger);
 
             // Update the file system cache
-            CacheImageSize(fullCachePath, size.Width, size.Height);
-
-            return imageSize;
-        }
+            Task.Run(() => _protobufSerializer.SerializeToFile(new[] { size.Width, size.Height }, fullCachePath));
 
-        /// <summary>
-        /// Caches image dimensions
-        /// </summary>
-        /// <param name="cachePath">The cache path.</param>
-        /// <param name="width">The width.</param>
-        /// <param name="height">The height.</param>
-        private void CacheImageSize(string cachePath, int width, int height)
-        {
-            var output = new[] { width, height };
-
-            _protobufSerializer.SerializeToFile(output, cachePath);
+            return new ImageSize { Width = size.Width, Height = size.Height };
         }
 
         /// <summary>
@@ -367,7 +377,7 @@ namespace MediaBrowser.Controller.Drawing
 
                 return video.Chapters[imageIndex].ImagePath;
             }
-            
+
             return item.GetImage(imageType);
         }
 
@@ -409,7 +419,7 @@ namespace MediaBrowser.Controller.Drawing
             {
                 throw new ArgumentNullException("imagePath");
             }
-            
+
             var metaFileEntry = item.ResolveArgs.GetMetaFileByPath(imagePath);
 
             // If we didn't the metafile entry, check the Season
@@ -440,38 +450,53 @@ namespace MediaBrowser.Controller.Drawing
 
             var croppedImagePath = CroppedImageCache.GetResourcePath(name, Path.GetExtension(originalImagePath));
 
-            if (!CroppedImageCache.ContainsFilePath(croppedImagePath))
+            if (CroppedImageCache.ContainsFilePath(croppedImagePath))
+            {
+                return croppedImagePath;
+            }
+
+            var semaphore = GetLock(croppedImagePath);
+
+            await semaphore.WaitAsync().ConfigureAwait(false);
+
+            // Check again in case of contention
+            if (CroppedImageCache.ContainsFilePath(croppedImagePath))
+            {
+                semaphore.Release();
+                return croppedImagePath;
+            }
+            
+            try
             {
                 using (var fileStream = File.OpenRead(originalImagePath))
                 {
-                    using (var originalImage = (Bitmap)Bitmap.FromStream(fileStream, true, false))
+                    using (var originalImage = (Bitmap)Image.FromStream(fileStream, true, false))
                     {
                         var outputFormat = originalImage.RawFormat;
 
                         using (var croppedImage = originalImage.CropWhitespace())
                         {
-                            await SaveImageToFile(croppedImage, outputFormat, croppedImagePath).ConfigureAwait(false);
+                            using (var outputStream = new FileStream(croppedImagePath, FileMode.Create, FileAccess.Write, FileShare.Read))
+                            {
+                                croppedImage.Save(outputFormat, outputStream, 100);
+                            }
                         }
                     }
                 }
             }
-
-            return croppedImagePath;
-        }
-
-        private async Task SaveImageToFile(Image image, ImageFormat outputFormat, string file)
-        {
-            using (var memoryStream = new MemoryStream())
+            catch (Exception ex)
             {
-                image.Save(outputFormat, memoryStream, 100);
+                // We have to have a catch-all here because some of the .net image methods throw a plain old Exception
+                _logger.ErrorException("Error cropping image {0}", ex, originalImagePath);
 
-                memoryStream.Position = 0;
-
-                using (var cacheFileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous))
-                {
-                    await memoryStream.CopyToAsync(cacheFileStream).ConfigureAwait(false);
-                }
+                return originalImagePath;
+            }
+            finally
+            {
+                semaphore.Release();
             }
+
+            return croppedImagePath;
         }
 
         /// <summary>
@@ -509,7 +534,23 @@ namespace MediaBrowser.Controller.Drawing
             // All enhanced images are saved as png to allow transparency
             var enhancedImagePath = EnhancedImageCache.GetResourcePath(cacheGuid + ".png");
 
-            if (!EnhancedImageCache.ContainsFilePath(enhancedImagePath))
+            if (EnhancedImageCache.ContainsFilePath(enhancedImagePath))
+            {
+                return enhancedImagePath;
+            }
+            
+            var semaphore = GetLock(enhancedImagePath);
+
+            await semaphore.WaitAsync().ConfigureAwait(false);
+
+            // Check again in case of contention
+            if (EnhancedImageCache.ContainsFilePath(enhancedImagePath))
+            {
+                semaphore.Release();
+                return enhancedImagePath;
+            }
+
+            try
             {
                 using (var fileStream = File.OpenRead(originalImagePath))
                 {
@@ -519,11 +560,18 @@ namespace MediaBrowser.Controller.Drawing
                         using (var newImage = await ExecuteImageEnhancers(supportedEnhancers, originalImage, item, imageType, imageIndex).ConfigureAwait(false))
                         {
                             //And then save it in the cache
-                            await SaveImageToFile(newImage, ImageFormat.Png, enhancedImagePath).ConfigureAwait(false);
+                            using (var outputStream = new FileStream(enhancedImagePath, FileMode.Create, FileAccess.Write, FileShare.Read))
+                            {
+                                newImage.Save(ImageFormat.Png, outputStream, 100);
+                            }
                         }
                     }
                 }
             }
+            finally
+            {
+                semaphore.Release();
+            }
 
             return enhancedImagePath;
         }
@@ -547,7 +595,7 @@ namespace MediaBrowser.Controller.Drawing
             {
                 throw new ArgumentNullException("imagePath");
             }
-            
+
             var dateModified = GetImageDateModified(item, imagePath);
 
             var supportedEnhancers = _kernel.ImageEnhancers.Where(i => i.Supports(item, imageType));
@@ -624,6 +672,19 @@ namespace MediaBrowser.Controller.Drawing
             return result;
         }
 
+        /// <summary>
+        /// Gets the lock.
+        /// </summary>
+        /// <param name="filename">The filename.</param>
+        /// <returns>System.Object.</returns>
+        private SemaphoreSlim GetLock(string filename)
+        {
+            return _locks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1));
+        }
+
+        /// <summary>
+        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+        /// </summary>
         public void Dispose()
         {
             Dispose(true);

+ 28 - 28
MediaBrowser.Controller/Library/DtoBuilder.cs

@@ -59,11 +59,21 @@ namespace MediaBrowser.Controller.Library
 
             var tasks = new List<Task>();
 
+            if (fields.Contains(ItemFields.Studios))
+            {
+                dto.Studios = item.Studios;
+            }
+
+            if (fields.Contains(ItemFields.People))
+            {
+                tasks.Add(AttachPeople(dto, item));
+            }
+
             if (fields.Contains(ItemFields.PrimaryImageAspectRatio))
             {
                 try
                 {
-                    tasks.Add(AttachPrimaryImageAspectRatio(dto, item));
+                    AttachPrimaryImageAspectRatio(dto, item);
                 }
                 catch (Exception ex)
                 {
@@ -72,16 +82,6 @@ namespace MediaBrowser.Controller.Library
                 }
             }
 
-            if (fields.Contains(ItemFields.Studios))
-            {
-                dto.Studios = item.Studios;
-            }
-
-            if (fields.Contains(ItemFields.People))
-            {
-                tasks.Add(AttachPeople(dto, item));
-            }
-
             AttachBasicFields(dto, item, fields);
 
             // Make sure all the tasks we kicked off have completed.
@@ -120,19 +120,6 @@ namespace MediaBrowser.Controller.Library
 
             var tasks = new List<Task>();
 
-            if (fields.Contains(ItemFields.PrimaryImageAspectRatio))
-            {
-                try
-                {
-                    tasks.Add(AttachPrimaryImageAspectRatio(dto, item));
-                }
-                catch (Exception ex)
-                {
-                    // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions
-                    _logger.ErrorException("Error generating PrimaryImageAspectRatio for {0}", ex, item.Name);
-                }
-            }
-
             if (fields.Contains(ItemFields.Studios))
             {
                 dto.Studios = item.Studios;
@@ -145,6 +132,19 @@ namespace MediaBrowser.Controller.Library
 
             tasks.Add(AttachUserSpecificInfo(dto, item, user, fields));
 
+            if (fields.Contains(ItemFields.PrimaryImageAspectRatio))
+            {
+                try
+                {
+                    AttachPrimaryImageAspectRatio(dto, item);
+                }
+                catch (Exception ex)
+                {
+                    // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions
+                    _logger.ErrorException("Error generating PrimaryImageAspectRatio for {0}", ex, item.Name);
+                }
+            }
+
             AttachBasicFields(dto, item, fields);
 
             // Make sure all the tasks we kicked off have completed.
@@ -199,7 +199,7 @@ namespace MediaBrowser.Controller.Library
         /// <param name="dto">The dto.</param>
         /// <param name="item">The item.</param>
         /// <returns>Task.</returns>
-        private async Task AttachPrimaryImageAspectRatio(IItemDto dto, BaseItem item)
+        private void AttachPrimaryImageAspectRatio(IItemDto dto, BaseItem item)
         {
             var path = item.PrimaryImagePath;
 
@@ -217,7 +217,7 @@ namespace MediaBrowser.Controller.Library
 
             try
             {
-                size = await Kernel.Instance.ImageManager.GetImageSize(path, dateModified).ConfigureAwait(false);
+                size = Kernel.Instance.ImageManager.GetImageSize(path, dateModified);
             }
             catch (FileNotFoundException)
             {
@@ -771,7 +771,7 @@ namespace MediaBrowser.Controller.Library
         /// <param name="user">The user.</param>
         /// <returns>DtoUser.</returns>
         /// <exception cref="System.ArgumentNullException">user</exception>
-        public async Task<UserDto> GetUserDto(User user)
+        public UserDto GetUserDto(User user)
         {
             if (user == null)
             {
@@ -796,7 +796,7 @@ namespace MediaBrowser.Controller.Library
 
                 try
                 {
-                    await AttachPrimaryImageAspectRatio(dto, user).ConfigureAwait(false);
+                    AttachPrimaryImageAspectRatio(dto, user);
                 }
                 catch (Exception ex)
                 {

+ 8 - 0
MediaBrowser.Server.Implementations/Library/UserManager.cs

@@ -300,10 +300,18 @@ namespace MediaBrowser.Server.Implementations.Library
 
             var activityDate = DateTime.UtcNow;
 
+            var lastActivityDate = user.LastActivityDate;
+
             user.LastActivityDate = activityDate;
 
             LogConnection(user.Id, clientType, deviceId, deviceName, activityDate);
 
+            // Don't log in the db anymore frequently than 10 seconds
+            if (lastActivityDate.HasValue && (activityDate - lastActivityDate.Value).TotalSeconds < 10)
+            {
+                return Task.FromResult(true);
+            }
+
             // Save this directly. No need to fire off all the events for this.
             return Kernel.UserRepository.SaveUser(user, CancellationToken.None);
         }

+ 3 - 3
MediaBrowser.Server.Implementations/Sqlite/SQLiteDisplayPreferencesRepository.cs

@@ -48,10 +48,10 @@ namespace MediaBrowser.Server.Implementations.Sqlite
         /// </summary>
         /// <param name="appPaths">The app paths.</param>
         /// <param name="protobufSerializer">The protobuf serializer.</param>
-        /// <param name="logger">The logger.</param>
+        /// <param name="logManager">The log manager.</param>
         /// <exception cref="System.ArgumentNullException">protobufSerializer</exception>
-        public SQLiteDisplayPreferencesRepository(IApplicationPaths appPaths, IProtobufSerializer protobufSerializer, ILogger logger)
-            : base(logger)
+        public SQLiteDisplayPreferencesRepository(IApplicationPaths appPaths, IProtobufSerializer protobufSerializer, ILogManager logManager)
+            : base(logManager)
         {
             if (protobufSerializer == null)
             {

+ 3 - 3
MediaBrowser.Server.Implementations/Sqlite/SQLiteItemRepository.cs

@@ -56,10 +56,10 @@ namespace MediaBrowser.Server.Implementations.Sqlite
         /// </summary>
         /// <param name="appPaths">The app paths.</param>
         /// <param name="jsonSerializer">The json serializer.</param>
-        /// <param name="logger">The logger.</param>
+        /// <param name="logManager">The log manager.</param>
         /// <exception cref="System.ArgumentNullException">appPaths</exception>
-        public SQLiteItemRepository(IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILogger logger)
-            : base(logger)
+        public SQLiteItemRepository(IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILogManager logManager)
+            : base(logManager)
         {
             if (appPaths == null)
             {

+ 5 - 5
MediaBrowser.Server.Implementations/Sqlite/SQLiteRepository.cs

@@ -46,16 +46,16 @@ namespace MediaBrowser.Server.Implementations.Sqlite
         /// <summary>
         /// Initializes a new instance of the <see cref="SqliteRepository" /> class.
         /// </summary>
-        /// <param name="logger">The logger.</param>
+        /// <param name="logManager">The log manager.</param>
         /// <exception cref="System.ArgumentNullException">logger</exception>
-        protected SqliteRepository(ILogger logger)
+        protected SqliteRepository(ILogManager logManager)
         {
-            if (logger == null)
+            if (logManager == null)
             {
-                throw new ArgumentNullException("logger");
+                throw new ArgumentNullException("logManager");
             }
 
-            Logger = logger;
+            Logger = logManager.GetLogger(GetType().Name);
         }
 
         /// <summary>

+ 3 - 3
MediaBrowser.Server.Implementations/Sqlite/SQLiteUserDataRepository.cs

@@ -49,10 +49,10 @@ namespace MediaBrowser.Server.Implementations.Sqlite
         /// </summary>
         /// <param name="appPaths">The app paths.</param>
         /// <param name="protobufSerializer">The protobuf serializer.</param>
-        /// <param name="logger">The logger.</param>
+        /// <param name="logManager">The log manager.</param>
         /// <exception cref="System.ArgumentNullException">protobufSerializer</exception>
-        public SQLiteUserDataRepository(IApplicationPaths appPaths, IProtobufSerializer protobufSerializer, ILogger logger)
-            : base(logger)
+        public SQLiteUserDataRepository(IApplicationPaths appPaths, IProtobufSerializer protobufSerializer, ILogManager logManager)
+            : base(logManager)
         {
             if (protobufSerializer == null)
             {

+ 3 - 3
MediaBrowser.Server.Implementations/Sqlite/SQLiteUserRepository.cs

@@ -50,10 +50,10 @@ namespace MediaBrowser.Server.Implementations.Sqlite
         /// </summary>
         /// <param name="appPaths">The app paths.</param>
         /// <param name="jsonSerializer">The json serializer.</param>
-        /// <param name="logger">The logger.</param>
+        /// <param name="logManager">The log manager.</param>
         /// <exception cref="System.ArgumentNullException">appPaths</exception>
-        public SQLiteUserRepository(IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILogger logger)
-            : base(logger)
+        public SQLiteUserRepository(IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILogManager logManager)
+            : base(logManager)
         {
             if (appPaths == null)
             {

+ 2 - 2
MediaBrowser.ServerApplication/EntryPoints/WebSocketEvents.cs

@@ -172,9 +172,9 @@ namespace MediaBrowser.ServerApplication.EntryPoints
         /// </summary>
         /// <param name="sender">The sender.</param>
         /// <param name="e">The e.</param>
-        async void userManager_UserUpdated(object sender, GenericEventArgs<User> e)
+        void userManager_UserUpdated(object sender, GenericEventArgs<User> e)
         {
-            var dto = await new DtoBuilder(_logger, _libraryManager, _userManager).GetUserDto(e.Argument).ConfigureAwait(false);
+            var dto = new DtoBuilder(_logger, _libraryManager, _userManager).GetUserDto(e.Argument);
 
             _serverManager.SendWebSocketMessage("UserUpdated", dto);
         }

+ 3 - 1
MediaBrowser.WebDashboard/Api/DashboardInfoWebSocketListener.cs

@@ -40,9 +40,11 @@ namespace MediaBrowser.WebDashboard.Api
         /// <summary>
         /// Initializes a new instance of the <see cref="DashboardInfoWebSocketListener" /> class.
         /// </summary>
+        /// <param name="appHost">The app host.</param>
         /// <param name="logger">The logger.</param>
         /// <param name="taskManager">The task manager.</param>
         /// <param name="userManager">The user manager.</param>
+        /// <param name="libraryManager">The library manager.</param>
         public DashboardInfoWebSocketListener(IServerApplicationHost appHost, ILogger logger, ITaskManager taskManager, IUserManager userManager, ILibraryManager libraryManager)
             : base(logger)
         {
@@ -59,7 +61,7 @@ namespace MediaBrowser.WebDashboard.Api
         /// <returns>Task{IEnumerable{TaskInfo}}.</returns>
         protected override Task<DashboardInfo> GetDataToSend(object state)
         {
-            return DashboardService.GetDashboardInfo(_appHost, Logger, _taskManager, _userManager, _libraryManager);
+            return Task.FromResult(DashboardService.GetDashboardInfo(_appHost, Logger, _taskManager, _userManager, _libraryManager));
         }
     }
 }

+ 4 - 5
MediaBrowser.WebDashboard/Api/DashboardService.cs

@@ -147,7 +147,7 @@ namespace MediaBrowser.WebDashboard.Api
         /// <returns>System.Object.</returns>
         public object Get(GetDashboardInfo request)
         {
-            return GetDashboardInfo(_appHost, Logger, _taskManager, _userManager, _libraryManager).Result;
+            return GetDashboardInfo(_appHost, Logger, _taskManager, _userManager, _libraryManager);
         }
 
         /// <summary>
@@ -159,14 +159,13 @@ namespace MediaBrowser.WebDashboard.Api
         /// <param name="userManager">The user manager.</param>
         /// <param name="libraryManager">The library manager.</param>
         /// <returns>DashboardInfo.</returns>
-        public static async Task<DashboardInfo> GetDashboardInfo(IServerApplicationHost appHost, ILogger logger, ITaskManager taskManager, IUserManager userManager, ILibraryManager libraryManager)
+        public static DashboardInfo GetDashboardInfo(IServerApplicationHost appHost, ILogger logger, ITaskManager taskManager, IUserManager userManager, ILibraryManager libraryManager)
         {
             var connections = userManager.RecentConnections.ToArray();
 
             var dtoBuilder = new DtoBuilder(logger, libraryManager, userManager);
 
-            var tasks = userManager.Users.Where(u => connections.Any(c => c.UserId == u.Id)).Select(dtoBuilder.GetUserDto);
-            var users = await Task.WhenAll(tasks).ConfigureAwait(false);
+            var users = userManager.Users.Where(u => connections.Any(c => c.UserId == u.Id)).Select(dtoBuilder.GetUserDto);
 
             return new DashboardInfo
             {
@@ -180,7 +179,7 @@ namespace MediaBrowser.WebDashboard.Api
 
                 ActiveConnections = connections,
 
-                Users = users
+                Users = users.ToArray()
             };
         }