diff --git a/Flow.Launcher.Infrastructure/Image/ImageLoader.cs b/Flow.Launcher.Infrastructure/Image/ImageLoader.cs index 86df01a3015..64d323de6c5 100644 --- a/Flow.Launcher.Infrastructure/Image/ImageLoader.cs +++ b/Flow.Launcher.Infrastructure/Image/ImageLoader.cs @@ -19,14 +19,14 @@ public static class ImageLoader private static readonly string ClassName = nameof(ImageLoader); private static readonly ImageCache ImageCache = new(); - private static SemaphoreSlim storageLock { get; } = new SemaphoreSlim(1, 1); + private static Lock storageLock { get; } = new(); private static BinaryStorage> _storage; private static readonly ConcurrentDictionary GuidToKey = new(); private static IImageHashGenerator _hashGenerator; private static readonly bool EnableImageHash = true; - public static ImageSource Image { get; } = new BitmapImage(new Uri(Constant.ImageIcon)); - public static ImageSource MissingImage { get; } = new BitmapImage(new Uri(Constant.MissingImgIcon)); - public static ImageSource LoadingImage { get; } = new BitmapImage(new Uri(Constant.LoadingImgIcon)); + public static ImageSource Image => ImageCache[Constant.ImageIcon, false]; + public static ImageSource MissingImage => ImageCache[Constant.MissingImgIcon, false]; + public static ImageSource LoadingImage => ImageCache[Constant.LoadingImgIcon, false]; public const int SmallIconSize = 64; public const int FullIconSize = 256; public const int FullImageSize = 320; @@ -36,20 +36,25 @@ public static class ImageLoader public static async Task InitializeAsync() { - _storage = new BinaryStorage>("Image"); - _hashGenerator = new ImageHashGenerator(); + var usage = await Task.Run(() => + { + _storage = new BinaryStorage>("Image"); + _hashGenerator = new ImageHashGenerator(); - var usage = await LoadStorageToConcurrentDictionaryAsync(); - _storage.ClearData(); + var usage = LoadStorageToConcurrentDictionary(); + _storage.ClearData(); - ImageCache.Initialize(usage); + ImageCache.Initialize(usage); - foreach (var icon in new[] { Constant.DefaultIcon, Constant.MissingImgIcon }) - { - ImageSource img = new BitmapImage(new Uri(icon)); - img.Freeze(); - ImageCache[icon, false] = img; - } + foreach (var icon in new[] { Constant.DefaultIcon, Constant.ImageIcon, Constant.MissingImgIcon, Constant.LoadingImgIcon }) + { + ImageSource img = new BitmapImage(new Uri(icon)); + img.Freeze(); + ImageCache[icon, false] = img; + } + + return usage; + }); _ = Task.Run(async () => { @@ -64,42 +69,26 @@ await Stopwatch.InfoAsync(ClassName, "Preload images cost", async () => }); } - public static async Task SaveAsync() + public static void Save() { - await storageLock.WaitAsync(); - - try - { - await _storage.SaveAsync(ImageCache.EnumerateEntries() - .Select(x => x.Key) - .ToList()); - } - catch (System.Exception e) + lock (storageLock) { - Log.Exception(ClassName, "Failed to save image cache to file", e); - } - finally - { - storageLock.Release(); + try + { + _storage.Save([.. ImageCache.EnumerateEntries().Select(x => x.Key)]); + } + catch (System.Exception e) + { + Log.Exception(ClassName, "Failed to save image cache to file", e); + } } } - public static async Task WaitSaveAsync() + private static List<(string, bool)> LoadStorageToConcurrentDictionary() { - await storageLock.WaitAsync(); - storageLock.Release(); - } - - private static async Task> LoadStorageToConcurrentDictionaryAsync() - { - await storageLock.WaitAsync(); - try - { - return await _storage.TryLoadAsync(new List<(string, bool)>()); - } - finally + lock (storageLock) { - storageLock.Release(); + return _storage.TryLoad([]); } } @@ -174,7 +163,7 @@ private static async ValueTask LoadInternalAsync(string path, bool Log.Exception(ClassName, $"Failed to get thumbnail for {path} on first try", e); Log.Exception(ClassName, $"Failed to get thumbnail for {path} on second try", e2); - ImageSource image = ImageCache[Constant.MissingImgIcon, false]; + ImageSource image = MissingImage; ImageCache[path, false] = image; imageResult = new ImageResult(image, ImageType.Error); } @@ -273,7 +262,7 @@ private static ImageResult GetThumbnailResult(ref string path, bool loadFullImag } else { - image = ImageCache[Constant.MissingImgIcon, false]; + image = MissingImage; path = Constant.MissingImgIcon; } diff --git a/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs b/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs index 48e6b55238c..15200f5aa38 100644 --- a/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs @@ -12,7 +12,7 @@ namespace Flow.Launcher.Infrastructure.Storage { /// - /// Stroage object using binary data + /// Storage object using binary data /// Normally, it has better performance, but not readable /// /// @@ -53,6 +53,45 @@ public BinaryStorage(string filename, string directoryPath = null!) FilePath = Path.Combine(DirectoryPath, $"{filename}{FileSuffix}"); } + public T TryLoad(T defaultData) + { + if (Data != null) return Data; + + if (File.Exists(FilePath)) + { + if (new FileInfo(FilePath).Length == 0) + { + Log.Error(ClassName, $"Zero length cache file <{FilePath}>"); + Data = defaultData; + Save(); + } + + var bytes = File.ReadAllBytes(FilePath); + Data = Deserialize(bytes, defaultData); + } + else + { + Log.Info(ClassName, "Cache file not exist, load default data"); + Data = defaultData; + Save(); + } + return Data; + } + + private T Deserialize(ReadOnlySpan bytes, T defaultData) + { + try + { + var t = MemoryPackSerializer.Deserialize(bytes); + return t ?? defaultData; + } + catch (System.Exception e) + { + Log.Exception(ClassName, $"Deserialize error for file <{FilePath}>", e); + return defaultData; + } + } + public async ValueTask TryLoadAsync(T defaultData) { if (Data != null) return Data; @@ -79,26 +118,31 @@ public async ValueTask TryLoadAsync(T defaultData) return Data; } - private static async ValueTask DeserializeAsync(Stream stream, T defaultData) + private async ValueTask DeserializeAsync(Stream stream, T defaultData) { try { var t = await MemoryPackSerializer.DeserializeAsync(stream); return t ?? defaultData; } - catch (System.Exception) + catch (System.Exception e) { - // Log.Exception($"|BinaryStorage.Deserialize|Deserialize error for file <{FilePath}>", e); + Log.Exception(ClassName, $"Deserialize error for file <{FilePath}>", e); return defaultData; } } public void Save() + { + Save(Data.NonNull()); + } + + public void Save(T data) { // User may delete the directory, so we need to check it FilesFolders.ValidateDirectory(DirectoryPath); - var serialized = MemoryPackSerializer.Serialize(Data); + var serialized = MemoryPackSerializer.Serialize(data); File.WriteAllBytes(FilePath, serialized); } @@ -107,15 +151,6 @@ public async ValueTask SaveAsync() await SaveAsync(Data.NonNull()); } - // ImageCache need to convert data into concurrent dictionary for usage, - // so we would better to clear the data - public void ClearData() - { - Data = default; - } - - // ImageCache storages data in its class, - // so we need to pass it to SaveAsync public async ValueTask SaveAsync(T data) { // User may delete the directory, so we need to check it @@ -124,5 +159,12 @@ public async ValueTask SaveAsync(T data) await using var stream = new FileStream(FilePath, FileMode.Create); await MemoryPackSerializer.SerializeAsync(stream, data); } + + // ImageCache need to convert data into concurrent dictionary for usage, + // so we would better to clear the data + public void ClearData() + { + Data = default; + } } } diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 8eb41e032fa..76603e7777b 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -18,7 +18,6 @@ using Flow.Launcher.Core.Resource; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Hotkey; -using Flow.Launcher.Infrastructure.Image; using Flow.Launcher.Infrastructure.DialogJump; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; @@ -358,7 +357,6 @@ private async void OnClosing(object sender, CancelEventArgs e) _notifyIcon.Visible = false; App.API.SaveAppAllSettings(); e.Cancel = true; - await ImageLoader.WaitSaveAsync(); await PluginManager.DisposePluginsAsync(); Notification.Uninstall(); // After plugins are all disposed, we shutdown application to close app diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index e0ed105cff9..0f66db773f5 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -74,7 +74,7 @@ public void ChangeQuery(string query, bool requery = false) } [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "")] - public async void RestartApp() + public void RestartApp() { _mainVM.Hide(); @@ -83,9 +83,6 @@ public async void RestartApp() // which will cause ungraceful exit SaveAppAllSettings(); - // Wait for all image caches to be saved before restarting - await ImageLoader.WaitSaveAsync(); - // Restart requires Squirrel's Update.exe to be present in the parent folder, // it is only published from the project's release pipeline. When debugging without it, // the project may not restart or just terminates. This is expected. @@ -115,8 +112,8 @@ public void SaveAppAllSettings() _settings.Save(); PluginManager.Save(); _mainVM.Save(); + ImageLoader.Save(); } - _ = ImageLoader.SaveAsync(); } public Task ReloadAllPluginData() => PluginManager.ReloadDataAsync();