diff --git a/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs index 102d0089f9a..4d988b837b6 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs @@ -189,5 +189,10 @@ public void StopLoadingBar() { _api.StopLoadingBar(); } + + public void SavePluginCaches() + { + _api.SavePluginCaches(); + } } } diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index aa6c54a9434..0ee268f5c56 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -13,6 +13,7 @@ using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; +using IRemovable = Flow.Launcher.Core.Storage.IRemovable; using ISavable = Flow.Launcher.Plugin.ISavable; namespace Flow.Launcher.Core.Plugin @@ -65,6 +66,7 @@ public static void Save() } API.SavePluginSettings(); + API.SavePluginCaches(); } public static async ValueTask DisposePluginsAsync() @@ -575,11 +577,11 @@ internal static async Task UninstallPluginAsync(PluginMetadata plugin, bool remo if (removePluginSettings) { - // For dotnet plugins, we need to remove their PluginJsonStorage instance - if (AllowedLanguage.IsDotNet(plugin.Language)) + // For dotnet plugins, we need to remove their PluginJsonStorage and PluginBinaryStorage instances + if (AllowedLanguage.IsDotNet(plugin.Language) && API is IRemovable removable) { - var method = API.GetType().GetMethod("RemovePluginSettings"); - method?.Invoke(API, new object[] { plugin.AssemblyName }); + removable.RemovePluginSettings(plugin.AssemblyName); + removable.RemovePluginCaches(plugin.PluginCacheDirectoryPath); } try diff --git a/Flow.Launcher.Core/Storage/IRemovable.cs b/Flow.Launcher.Core/Storage/IRemovable.cs new file mode 100644 index 00000000000..bcf1cdd5e48 --- /dev/null +++ b/Flow.Launcher.Core/Storage/IRemovable.cs @@ -0,0 +1,19 @@ +namespace Flow.Launcher.Core.Storage; + +/// +/// Remove storage instances from instance +/// +public interface IRemovable +{ + /// + /// Remove all instances of one plugin + /// + /// + public void RemovePluginSettings(string assemblyName); + + /// + /// Remove all instances of one plugin + /// + /// + public void RemovePluginCaches(string cacheDirectory); +} diff --git a/Flow.Launcher.Infrastructure/Image/ImageLoader.cs b/Flow.Launcher.Infrastructure/Image/ImageLoader.cs index 01abf8995fc..41a33104b66 100644 --- a/Flow.Launcher.Infrastructure/Image/ImageLoader.cs +++ b/Flow.Launcher.Infrastructure/Image/ImageLoader.cs @@ -34,6 +34,7 @@ public static async Task InitializeAsync() _hashGenerator = new ImageHashGenerator(); var usage = await LoadStorageToConcurrentDictionaryAsync(); + _storage.ClearData(); ImageCache.Initialize(usage); diff --git a/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs b/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs index a8d5f5d6218..43bb8dadecb 100644 --- a/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs @@ -2,9 +2,12 @@ using System.Threading.Tasks; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; using MemoryPack; +#nullable enable + namespace Flow.Launcher.Infrastructure.Storage { /// @@ -12,44 +15,55 @@ namespace Flow.Launcher.Infrastructure.Storage /// Normally, it has better performance, but not readable /// /// - /// It utilize MemoryPack, which means the object must be MemoryPackSerializable + /// It utilizes MemoryPack, which means the object must be MemoryPackSerializable /// - public class BinaryStorage + public class BinaryStorage : ISavable { + protected T? Data; + public const string FileSuffix = ".cache"; + protected string FilePath { get; init; } = null!; + + protected string DirectoryPath { get; init; } = null!; + // Let the derived class to set the file path - public BinaryStorage(string filename, string directoryPath = null) + protected BinaryStorage() { - directoryPath ??= DataLocation.CacheDirectory; - FilesFolders.ValidateDirectory(directoryPath); - - FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}"); } - public string FilePath { get; } + public BinaryStorage(string filename) + { + DirectoryPath = DataLocation.CacheDirectory; + FilesFolders.ValidateDirectory(DirectoryPath); + + FilePath = Path.Combine(DirectoryPath, $"{filename}{FileSuffix}"); + } public async ValueTask TryLoadAsync(T defaultData) { + if (Data != null) return Data; + if (File.Exists(FilePath)) { if (new FileInfo(FilePath).Length == 0) { Log.Error($"|BinaryStorage.TryLoad|Zero length cache file <{FilePath}>"); - await SaveAsync(defaultData); - return defaultData; + Data = defaultData; + await SaveAsync(); } await using var stream = new FileStream(FilePath, FileMode.Open); - var d = await DeserializeAsync(stream, defaultData); - return d; + Data = await DeserializeAsync(stream, defaultData); } else { Log.Info("|BinaryStorage.TryLoad|Cache file not exist, load default data"); - await SaveAsync(defaultData); - return defaultData; + Data = defaultData; + await SaveAsync(); } + + return Data; } private static async ValueTask DeserializeAsync(Stream stream, T defaultData) @@ -57,7 +71,7 @@ private static async ValueTask DeserializeAsync(Stream stream, T defaultData) try { var t = await MemoryPackSerializer.DeserializeAsync(stream); - return t; + return t ?? defaultData; } catch (System.Exception) { @@ -66,6 +80,27 @@ private static async ValueTask DeserializeAsync(Stream stream, T defaultData) } } + public void Save() + { + var serialized = MemoryPackSerializer.Serialize(Data); + + File.WriteAllBytes(FilePath, serialized); + } + + 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) { await using var stream = new FileStream(FilePath, FileMode.Create); diff --git a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs index a3488124b92..cdf3ae90962 100644 --- a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs @@ -1,18 +1,20 @@ -#nullable enable -using System; +using System; using System.Globalization; using System.IO; using System.Text.Json; using System.Threading.Tasks; using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; +#nullable enable + namespace Flow.Launcher.Infrastructure.Storage { /// /// Serialize object using json format. /// - public class JsonStorage where T : new() + public class JsonStorage : ISavable where T : new() { protected T? Data; diff --git a/Flow.Launcher.Infrastructure/Storage/PluginBinaryStorage.cs b/Flow.Launcher.Infrastructure/Storage/PluginBinaryStorage.cs new file mode 100644 index 00000000000..d18060e3df0 --- /dev/null +++ b/Flow.Launcher.Infrastructure/Storage/PluginBinaryStorage.cs @@ -0,0 +1,49 @@ +using System.IO; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.DependencyInjection; +using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.SharedCommands; + +namespace Flow.Launcher.Infrastructure.Storage +{ + public class PluginBinaryStorage : BinaryStorage where T : new() + { + private static readonly string ClassName = "PluginBinaryStorage"; + + // We should not initialize API in static constructor because it will create another API instance + private static IPublicAPI api = null; + private static IPublicAPI API => api ??= Ioc.Default.GetRequiredService(); + + public PluginBinaryStorage(string cacheName, string cacheDirectory) + { + DirectoryPath = cacheDirectory; + FilesFolders.ValidateDirectory(DirectoryPath); + + FilePath = Path.Combine(DirectoryPath, $"{cacheName}{FileSuffix}"); + } + + public new void Save() + { + try + { + base.Save(); + } + catch (System.Exception e) + { + API.LogException(ClassName, $"Failed to save plugin caches to path: {FilePath}", e); + } + } + + public new async Task SaveAsync() + { + try + { + await base.SaveAsync(); + } + catch (System.Exception e) + { + API.LogException(ClassName, $"Failed to save plugin caches to path: {FilePath}", e); + } + } + } +} diff --git a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs index 7a1869fd687..a3020b60725 100644 --- a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs +++ b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs @@ -352,6 +352,38 @@ public interface IPublicAPI public void StopLoadingBar(); /// + /// Save all Flow's plugins caches + /// + void SavePluginCaches(); + + /// + /// Load BinaryStorage for current plugin's cache. This is the method used to load cache from binary in Flow. + /// When the file is not exist, it will create a new instance for the specific type. + /// + /// Type for deserialization + /// Cache file name + /// Cache directory from plugin metadata + /// Default data to return + /// + /// + /// BinaryStorage utilizes MemoryPack, which means the object must be MemoryPackSerializable + /// + Task LoadCacheBinaryStorageAsync(string cacheName, string cacheDirectory, T defaultData) where T : new(); + + /// + /// Save BinaryStorage for current plugin's cache. This is the method used to save cache to binary in Flow.Launcher + /// This method will save the original instance loaded with LoadCacheBinaryStorageAsync. + /// This API call is for manually Save. Flow will automatically save all cache type that has called LoadCacheBinaryStorageAsync or SaveCacheBinaryStorageAsync previously. + /// + /// Type for Serialization + /// Cache file name + /// Cache directory from plugin metadata + /// + /// + /// BinaryStorage utilizes MemoryPack, which means the object must be MemoryPackSerializable + /// + Task SaveCacheBinaryStorageAsync(string cacheName, string cacheDirectory) where T : new(); + /// Load image from path. Support local, remote and data:image url. /// If image path is missing, it will return a missing icon. /// diff --git a/Flow.Launcher.Plugin/Interfaces/ISavable.cs b/Flow.Launcher.Plugin/Interfaces/ISavable.cs index 77bd304e4ea..38cbf8e08f5 100644 --- a/Flow.Launcher.Plugin/Interfaces/ISavable.cs +++ b/Flow.Launcher.Plugin/Interfaces/ISavable.cs @@ -1,18 +1,21 @@ -namespace Flow.Launcher.Plugin +namespace Flow.Launcher.Plugin { /// - /// Inherit this interface if additional data e.g. cache needs to be saved. + /// Inherit this interface if you need to save additional data which is not a setting or cache, + /// please implement this interface. /// /// /// For storing plugin settings, prefer - /// or . - /// Once called, your settings will be automatically saved by Flow. + /// or . + /// For storing plugin caches, prefer + /// or . + /// Once called, those settings and caches will be automatically saved by Flow. /// public interface ISavable : IFeatures { /// - /// Save additional plugin data, such as cache. + /// Save additional plugin data. /// void Save(); } -} \ No newline at end of file +} diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index 4afd0327b21..a523f90bb8f 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -16,6 +16,7 @@ using Flow.Launcher.Core.Plugin; using Flow.Launcher.Core.Resource; using Flow.Launcher.Core.ExternalPlugins; +using Flow.Launcher.Core.Storage; using Flow.Launcher.Helper; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Http; @@ -33,7 +34,7 @@ namespace Flow.Launcher { - public class PublicAPIInstance : IPublicAPI + public class PublicAPIInstance : IPublicAPI, IRemovable { private readonly Settings _settings; private readonly Internationalization _translater; @@ -223,20 +224,17 @@ public void RemovePluginSettings(string assemblyName) var name = value.GetType().GetField("AssemblyName")?.GetValue(value)?.ToString(); if (name == assemblyName) { - _pluginJsonStorages.Remove(key, out var _); + _pluginJsonStorages.TryRemove(key, out var _); } } } - /// - /// Save plugin settings. - /// public void SavePluginSettings() { foreach (var value in _pluginJsonStorages.Values) { - var method = value.GetType().GetMethod("Save"); - method?.Invoke(value, null); + var savable = value as ISavable; + savable?.Save(); } } @@ -258,14 +256,6 @@ public void SavePluginSettings() ((PluginJsonStorage)_pluginJsonStorages[type]).Save(); } - public void SaveJsonStorage(T settings) where T : new() - { - var type = typeof(T); - _pluginJsonStorages[type] = new PluginJsonStorage(settings); - - ((PluginJsonStorage)_pluginJsonStorages[type]).Save(); - } - public void OpenDirectory(string DirectoryPath, string FileNameOrFilePath = null) { using var explorer = new Process(); @@ -370,6 +360,48 @@ public MessageBoxResult ShowMsgBox(string messageBoxText, string caption = "", public Task ShowProgressBoxAsync(string caption, Func, Task> reportProgressAsync, Action cancelProgress = null) => ProgressBoxEx.ShowAsync(caption, reportProgressAsync, cancelProgress); + private readonly ConcurrentDictionary<(string, string, Type), object> _pluginBinaryStorages = new(); + + public void RemovePluginCaches(string cacheDirectory) + { + foreach (var keyValuePair in _pluginBinaryStorages) + { + var key = keyValuePair.Key; + var currentCacheDirectory = key.Item2; + if (cacheDirectory == currentCacheDirectory) + { + _pluginBinaryStorages.TryRemove(key, out var _); + } + } + } + + public void SavePluginCaches() + { + foreach (var value in _pluginBinaryStorages.Values) + { + var savable = value as ISavable; + savable?.Save(); + } + } + + public async Task LoadCacheBinaryStorageAsync(string cacheName, string cacheDirectory, T defaultData) where T : new() + { + var type = typeof(T); + if (!_pluginBinaryStorages.ContainsKey((cacheName, cacheDirectory, type))) + _pluginBinaryStorages[(cacheName, cacheDirectory, type)] = new PluginBinaryStorage(cacheName, cacheDirectory); + + return await ((PluginBinaryStorage)_pluginBinaryStorages[(cacheName, cacheDirectory, type)]).TryLoadAsync(defaultData); + } + + public async Task SaveCacheBinaryStorageAsync(string cacheName, string cacheDirectory) where T : new() + { + var type = typeof(T); + if (!_pluginBinaryStorages.ContainsKey((cacheName, cacheDirectory, type))) + _pluginBinaryStorages[(cacheName, cacheDirectory, type)] = new PluginBinaryStorage(cacheName, cacheDirectory); + + await ((PluginBinaryStorage)_pluginBinaryStorages[(cacheName, cacheDirectory, type)]).SaveAsync(); + } + public ValueTask LoadImageAsync(string path, bool loadFullImage = false, bool cacheImage = true) => ImageLoader.LoadAsync(path, loadFullImage, cacheImage); diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs index 73b4ae9e65c..a50868b69dd 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -7,7 +7,6 @@ using System.Threading.Tasks; using System.Windows.Controls; using Flow.Launcher.Infrastructure.Logger; -using Flow.Launcher.Infrastructure.Storage; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin.Program.Programs; using Flow.Launcher.Plugin.Program.Views; @@ -19,18 +18,19 @@ namespace Flow.Launcher.Plugin.Program { - public class Main : ISettingProvider, IAsyncPlugin, IPluginI18n, IContextMenu, ISavable, IAsyncReloadable, - IDisposable + public class Main : ISettingProvider, IAsyncPlugin, IPluginI18n, IContextMenu, IAsyncReloadable, IDisposable { - internal static Win32[] _win32s { get; set; } - internal static UWPApp[] _uwps { get; set; } - internal static Settings _settings { get; set; } + private const string Win32CacheName = "Win32"; + private const string UwpCacheName = "UWP"; + internal static List _win32s { get; private set; } + internal static List _uwps { get; private set; } + internal static Settings _settings { get; private set; } - internal static PluginInitContext Context { get; private set; } + internal static SemaphoreSlim _win32sLock = new(1, 1); + internal static SemaphoreSlim _uwpsLock = new(1, 1); - private static BinaryStorage _win32Storage; - private static BinaryStorage _uwpStorage; + internal static PluginInitContext Context { get; private set; } private static readonly List emptyResults = new(); @@ -77,22 +77,15 @@ public class Main : ISettingProvider, IAsyncPlugin, IPluginI18n, IContextMenu, I private static readonly string WindowsAppPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "WindowsApps"); - static Main() - { - } - - public void Save() - { - _win32Storage.SaveAsync(_win32s); - _uwpStorage.SaveAsync(_uwps); - } - public async Task> QueryAsync(Query query, CancellationToken token) { var result = await cache.GetOrCreateAsync(query.Search, async entry => { - var resultList = await Task.Run(() => + var resultList = await Task.Run(async () => { + await _win32sLock.WaitAsync(token); + await _uwpsLock.WaitAsync(token); + try { // Collect all UWP Windows app directories @@ -119,7 +112,11 @@ public async Task> QueryAsync(Query query, CancellationToken token) Log.Debug("|Flow.Launcher.Plugin.Program.Main|Query operation cancelled"); return emptyResults; } - + finally + { + _uwpsLock.Release(); + _win32sLock.Release(); + } }, token); resultList = resultList.Any() ? resultList : emptyResults; @@ -189,9 +186,12 @@ public async Task InitAsync(PluginInitContext context) _settings = context.API.LoadSettingJsonStorage(); + var _win32sCount = 0; + var _uwpsCount = 0; await Stopwatch.NormalAsync("|Flow.Launcher.Plugin.Program.Main|Preload programs cost", async () => { - FilesFolders.ValidateDirectory(Context.CurrentPluginMetadata.PluginCacheDirectoryPath); + var pluginCacheDirectory = Context.CurrentPluginMetadata.PluginCacheDirectoryPath; + FilesFolders.ValidateDirectory(pluginCacheDirectory); static void MoveFile(string sourcePath, string destinationPath) { @@ -236,22 +236,27 @@ static void MoveFile(string sourcePath, string destinationPath) } // Move old cache files to the new cache directory - var oldWin32CacheFile = Path.Combine(DataLocation.CacheDirectory, $"Win32.cache"); - var newWin32CacheFile = Path.Combine(Context.CurrentPluginMetadata.PluginCacheDirectoryPath, $"Win32.cache"); + var oldWin32CacheFile = Path.Combine(DataLocation.CacheDirectory, $"{Win32CacheName}.cache"); + var newWin32CacheFile = Path.Combine(pluginCacheDirectory, $"{Win32CacheName}.cache"); MoveFile(oldWin32CacheFile, newWin32CacheFile); - var oldUWPCacheFile = Path.Combine(DataLocation.CacheDirectory, $"UWP.cache"); - var newUWPCacheFile = Path.Combine(Context.CurrentPluginMetadata.PluginCacheDirectoryPath, $"UWP.cache"); + var oldUWPCacheFile = Path.Combine(DataLocation.CacheDirectory, $"{UwpCacheName}.cache"); + var newUWPCacheFile = Path.Combine(pluginCacheDirectory, $"{UwpCacheName}.cache"); MoveFile(oldUWPCacheFile, newUWPCacheFile); - _win32Storage = new BinaryStorage("Win32", Context.CurrentPluginMetadata.PluginCacheDirectoryPath); - _win32s = await _win32Storage.TryLoadAsync(Array.Empty()); - _uwpStorage = new BinaryStorage("UWP", Context.CurrentPluginMetadata.PluginCacheDirectoryPath); - _uwps = await _uwpStorage.TryLoadAsync(Array.Empty()); + await _win32sLock.WaitAsync(); + _win32s = await context.API.LoadCacheBinaryStorageAsync(Win32CacheName, pluginCacheDirectory, new List()); + _win32sCount = _win32s.Count; + _win32sLock.Release(); + + await _uwpsLock.WaitAsync(); + _uwps = await context.API.LoadCacheBinaryStorageAsync(UwpCacheName, pluginCacheDirectory, new List()); + _uwpsCount = _uwps.Count; + _uwpsLock.Release(); }); - Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload win32 programs <{_win32s.Length}>"); - Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload uwps <{_uwps.Length}>"); + Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload win32 programs <{_win32sCount}>"); + Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload uwps <{_uwpsCount}>"); - bool cacheEmpty = !_win32s.Any() || !_uwps.Any(); + var cacheEmpty = _win32sCount == 0 || _uwpsCount == 0; if (cacheEmpty || _settings.LastIndexTime.AddHours(30) < DateTime.Now) { @@ -273,36 +278,69 @@ static void WatchProgramUpdate() } } - public static void IndexWin32Programs() + public static async Task IndexWin32ProgramsAsync() { - var win32S = Win32.All(_settings); - _win32s = win32S; - ResetCache(); - _win32Storage.SaveAsync(_win32s); - _settings.LastIndexTime = DateTime.Now; + await _win32sLock.WaitAsync(); + try + { + var win32S = Win32.All(_settings); + _win32s.Clear(); + foreach (var win32 in win32S) + { + _win32s.Add(win32); + } + ResetCache(); + await Context.API.SaveCacheBinaryStorageAsync>(Win32CacheName, Context.CurrentPluginMetadata.PluginCacheDirectoryPath); + _settings.LastIndexTime = DateTime.Now; + } + catch (Exception e) + { + Log.Exception("|Flow.Launcher.Plugin.Program.Main|Failed to index Win32 programs", e); + } + finally + { + _win32sLock.Release(); + } } - public static void IndexUwpPrograms() + public static async Task IndexUwpProgramsAsync() { - var applications = UWPPackage.All(_settings); - _uwps = applications; - ResetCache(); - _uwpStorage.SaveAsync(_uwps); - _settings.LastIndexTime = DateTime.Now; + await _uwpsLock.WaitAsync(); + try + { + var uwps = UWPPackage.All(_settings); + _uwps.Clear(); + foreach (var uwp in uwps) + { + _uwps.Add(uwp); + } + ResetCache(); + await Context.API.SaveCacheBinaryStorageAsync>(UwpCacheName, Context.CurrentPluginMetadata.PluginCacheDirectoryPath); + _settings.LastIndexTime = DateTime.Now; + } + catch (Exception e) + { + Log.Exception("|Flow.Launcher.Plugin.Program.Main|Failed to index Uwp programs", e); + } + finally + { + _uwpsLock.Release(); + } } public static async Task IndexProgramsAsync() { - var a = Task.Run(() => + var win32Task = Task.Run(async () => { - Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Win32Program index cost", IndexWin32Programs); + await Stopwatch.NormalAsync("|Flow.Launcher.Plugin.Program.Main|Win32Program index cost", IndexWin32ProgramsAsync); }); - var b = Task.Run(() => + var uwpTask = Task.Run(async () => { - Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|UWPProgram index cost", IndexUwpPrograms); + await Stopwatch.NormalAsync("|Flow.Launcher.Plugin.Program.Main|UWPProgram index cost", IndexUwpProgramsAsync); }); - await Task.WhenAll(a, b).ConfigureAwait(false); + + await Task.WhenAll(win32Task, uwpTask).ConfigureAwait(false); } internal static void ResetCache() @@ -314,7 +352,7 @@ internal static void ResetCache() public Control CreateSettingPanel() { - return new ProgramSetting(Context, _settings, _win32s, _uwps); + return new ProgramSetting(Context, _settings); } public string GetTranslatedPluginTitle() @@ -342,7 +380,7 @@ public List LoadContextMenus(Result selectedResult) Title = Context.API.GetTranslation("flowlauncher_plugin_program_disable_program"), Action = c => { - DisableProgram(program); + _ = DisableProgramAsync(program); Context.API.ShowMsg( Context.API.GetTranslation("flowlauncher_plugin_program_disable_dlgtitle_success"), Context.API.GetTranslation( @@ -358,30 +396,43 @@ public List LoadContextMenus(Result selectedResult) return menuOptions; } - private static void DisableProgram(IProgram programToDelete) + private static async Task DisableProgramAsync(IProgram programToDelete) { if (_settings.DisabledProgramSources.Any(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier)) return; + await _uwpsLock.WaitAsync(); if (_uwps.Any(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier)) { var program = _uwps.First(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier); program.Enabled = false; _settings.DisabledProgramSources.Add(new ProgramSource(program)); - _ = Task.Run(() => - { - IndexUwpPrograms(); - }); + _uwpsLock.Release(); + + // Reindex UWP programs + _ = Task.Run(IndexUwpProgramsAsync); + return; } - else if (_win32s.Any(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier)) + else + { + _uwpsLock.Release(); + } + + await _win32sLock.WaitAsync(); + if (_win32s.Any(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier)) { var program = _win32s.First(x => x.UniqueIdentifier == programToDelete.UniqueIdentifier); program.Enabled = false; _settings.DisabledProgramSources.Add(new ProgramSource(program)); - _ = Task.Run(() => - { - IndexWin32Programs(); - }); + _win32sLock.Release(); + + // Reindex Win32 programs + _ = Task.Run(IndexWin32ProgramsAsync); + return; + } + else + { + _win32sLock.Release(); } } diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs index 654897cc57f..bf100ed7ee3 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs @@ -317,7 +317,7 @@ public static async Task WatchPackageChange() { await Task.Delay(3000).ConfigureAwait(false); PackageChangeChannel.Reader.TryRead(out _); - await Task.Run(Main.IndexUwpPrograms); + await Task.Run(Main.IndexUwpProgramsAsync); } } } diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs index a64a708efd3..06be2a628cf 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs @@ -797,7 +797,7 @@ public static async Task MonitorDirectoryChangeAsync() { } - await Task.Run(Main.IndexWin32Programs); + await Task.Run(Main.IndexWin32ProgramsAsync); } } diff --git a/Plugins/Flow.Launcher.Plugin.Program/Views/Commands/ProgramSettingDisplay.cs b/Plugins/Flow.Launcher.Plugin.Program/Views/Commands/ProgramSettingDisplay.cs index e4d7c323a5f..b89a2a6ba9e 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Views/Commands/ProgramSettingDisplay.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Views/Commands/ProgramSettingDisplay.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Flow.Launcher.Plugin.Program.Views.Models; namespace Flow.Launcher.Plugin.Program.Views.Commands @@ -15,21 +16,24 @@ internal static List LoadProgramSources() .ToList(); } - internal static void DisplayAllPrograms() + internal static async Task DisplayAllProgramsAsync() { + await Main._win32sLock.WaitAsync(); var win32 = Main._win32s .Where(t1 => !ProgramSetting.ProgramSettingDisplayList.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier)) .Select(x => new ProgramSource(x)); + ProgramSetting.ProgramSettingDisplayList.AddRange(win32); + Main._win32sLock.Release(); + await Main._uwpsLock.WaitAsync(); var uwp = Main._uwps .Where(t1 => !ProgramSetting.ProgramSettingDisplayList.Any(x => x.UniqueIdentifier == t1.UniqueIdentifier)) .Select(x => new ProgramSource(x)); - - ProgramSetting.ProgramSettingDisplayList.AddRange(win32); ProgramSetting.ProgramSettingDisplayList.AddRange(uwp); + Main._uwpsLock.Release(); } - internal static void SetProgramSourcesStatus(List selectedProgramSourcesToDisable, bool status) + internal static async Task SetProgramSourcesStatusAsync(List selectedProgramSourcesToDisable, bool status) { foreach(var program in ProgramSetting.ProgramSettingDisplayList) { @@ -39,14 +43,17 @@ internal static void SetProgramSourcesStatus(List selectedProgram } } - foreach(var program in Main._win32s) + await Main._win32sLock.WaitAsync(); + foreach (var program in Main._win32s) { if (selectedProgramSourcesToDisable.Any(x => x.UniqueIdentifier == program.UniqueIdentifier && program.Enabled != status)) { program.Enabled = status; } } + Main._win32sLock.Release(); + await Main._uwpsLock.WaitAsync(); foreach (var program in Main._uwps) { if (selectedProgramSourcesToDisable.Any(x => x.UniqueIdentifier == program.UniqueIdentifier && program.Enabled != status)) @@ -54,6 +61,7 @@ internal static void SetProgramSourcesStatus(List selectedProgram program.Enabled = status; } } + Main._uwpsLock.Release(); } internal static void StoreDisabledInSettings() @@ -72,12 +80,22 @@ internal static void RemoveDisabledFromSettings() Main._settings.DisabledProgramSources.RemoveAll(t1 => t1.Enabled); } - internal static bool IsReindexRequired(this List selectedItems) + internal static async Task IsReindexRequiredAsync(this List selectedItems) { // Not in cache - if (selectedItems.Any(t1 => t1.Enabled && !Main._uwps.Any(x => t1.UniqueIdentifier == x.UniqueIdentifier)) + await Main._win32sLock.WaitAsync(); + await Main._uwpsLock.WaitAsync(); + try + { + if (selectedItems.Any(t1 => t1.Enabled && !Main._uwps.Any(x => t1.UniqueIdentifier == x.UniqueIdentifier)) && selectedItems.Any(t1 => t1.Enabled && !Main._win32s.Any(x => t1.UniqueIdentifier == x.UniqueIdentifier))) - return true; + return true; + } + finally + { + Main._win32sLock.Release(); + Main._uwpsLock.Release(); + } // ProgramSources holds list of user added directories, // so when we enable/disable we need to reindex to show/not show the programs diff --git a/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs b/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs index 91864cb680c..c42bd4f305d 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs @@ -18,8 +18,8 @@ namespace Flow.Launcher.Plugin.Program.Views /// public partial class ProgramSetting : UserControl { - private PluginInitContext context; - private Settings _settings; + private readonly PluginInitContext context; + private readonly Settings _settings; private GridViewColumnHeader _lastHeaderClicked; private ListSortDirection _lastDirection; @@ -109,7 +109,7 @@ public bool EnableUWP public bool ShowUWPCheckbox => UWPPackage.SupportUWP(); - public ProgramSetting(PluginInitContext context, Settings settings, Win32[] win32s, UWPApp[] uwps) + public ProgramSetting(PluginInitContext context, Settings settings) { this.context = context; _settings = settings; @@ -183,7 +183,7 @@ private void btnEditProgramSource_OnClick(object sender, RoutedEventArgs e) EditProgramSource(selectedProgramSource); } - private void EditProgramSource(ProgramSource selectedProgramSource) + private async void EditProgramSource(ProgramSource selectedProgramSource) { if (selectedProgramSource == null) { @@ -202,13 +202,13 @@ private void EditProgramSource(ProgramSource selectedProgramSource) { if (selectedProgramSource.Enabled) { - ProgramSettingDisplay.SetProgramSourcesStatus(new List { selectedProgramSource }, + await ProgramSettingDisplay.SetProgramSourcesStatusAsync(new List { selectedProgramSource }, true); // sync status in win32, uwp and disabled ProgramSettingDisplay.RemoveDisabledFromSettings(); } else { - ProgramSettingDisplay.SetProgramSourcesStatus(new List { selectedProgramSource }, + await ProgramSettingDisplay.SetProgramSourcesStatusAsync(new List { selectedProgramSource }, false); ProgramSettingDisplay.StoreDisabledInSettings(); } @@ -277,14 +277,14 @@ private void programSourceView_Drop(object sender, DragEventArgs e) } } - private void btnLoadAllProgramSource_OnClick(object sender, RoutedEventArgs e) + private async void btnLoadAllProgramSource_OnClick(object sender, RoutedEventArgs e) { - ProgramSettingDisplay.DisplayAllPrograms(); + await ProgramSettingDisplay.DisplayAllProgramsAsync(); ViewRefresh(); } - private void btnProgramSourceStatus_OnClick(object sender, RoutedEventArgs e) + private async void btnProgramSourceStatus_OnClick(object sender, RoutedEventArgs e) { var selectedItems = programSourceView .SelectedItems.Cast() @@ -311,18 +311,18 @@ private void btnProgramSourceStatus_OnClick(object sender, RoutedEventArgs e) } else if (HasMoreOrEqualEnabledItems(selectedItems)) { - ProgramSettingDisplay.SetProgramSourcesStatus(selectedItems, false); + await ProgramSettingDisplay.SetProgramSourcesStatusAsync(selectedItems, false); ProgramSettingDisplay.StoreDisabledInSettings(); } else { - ProgramSettingDisplay.SetProgramSourcesStatus(selectedItems, true); + await ProgramSettingDisplay.SetProgramSourcesStatusAsync(selectedItems, true); ProgramSettingDisplay.RemoveDisabledFromSettings(); } - if (selectedItems.IsReindexRequired()) + if (await selectedItems.IsReindexRequiredAsync()) ReIndexing(); programSourceView.SelectedItems.Clear();