Skip to content

Commit 0fba556

Browse files
authored
Hot (un)load extensions on (un)install (microsoft#375)
Closes microsoft#370 The DevHome code was great for "I need something that can lookup extensions and enumerate all of them". However, the DevHome code is a very blunt hammer when it comes to extensions. The only thing it tracks is "packages changed", and if it gets one of those, it just blows away all the extensions and rebuilds them. Yikes. This PR changes `ExtensionService` to be a scalpel. We'll keep `_installedExtensions` fresh. When we get a package install, we'll add only that package's extension to our cache, and let the `TopLevelCommandManager` know. Similarly for updates and uninstalls. That way, we can exactly change the top-level list as needed, rather than bluntly forcing all the extensions to reload. In the middle of all this, I fixed a bug where uninstalling an extension, then reloading would just fail to load extensions. This is because the old code would clear out the **whole** list of extensions when _one_ was uninstalled. That created a race where we'd be parsing the new list of all the extensions (from the reload), get an uninstall event, clear the list, then InvalidOperation as the list of extensions was modified during enumeration. There's a bunch more locking in here. This might drive-by microsoft#324 but hard to be sure. Related to microsoft#89
1 parent 899c233 commit 0fba556

File tree

10 files changed

+314
-144
lines changed

10 files changed

+314
-144
lines changed

src/modules/cmdpal/Microsoft.CmdPal.Common/Services/IExtensionService.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
// The Microsoft Corporation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5-
using System;
65
using System.Collections.Generic;
76
using System.Threading.Tasks;
7+
using Windows.Foundation;
88

99
namespace Microsoft.CmdPal.Common.Services;
1010

@@ -19,7 +19,9 @@ public interface IExtensionService
1919

2020
Task SignalStopExtensionsAsync();
2121

22-
public event EventHandler OnExtensionsChanged;
22+
public event TypedEventHandler<IExtensionService, IEnumerable<IExtensionWrapper>>? OnExtensionAdded;
23+
24+
public event TypedEventHandler<IExtensionService, IEnumerable<IExtensionWrapper>>? OnExtensionRemoved;
2325

2426
public void EnableExtension(string extensionUniqueId);
2527

src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPaletteHost.cs

+8
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Diagnostics;
77
using Microsoft.CmdPal.Common.Services;
88
using Microsoft.CmdPal.Extensions;
9+
using Microsoft.CmdPal.Extensions.Helpers;
910
using Windows.Foundation;
1011

1112
namespace Microsoft.CmdPal.UI.ViewModels;
@@ -173,4 +174,11 @@ public void ProcessHideStatusMessage(IStatusMessage message)
173174
}
174175

175176
public void SetHostHwnd(ulong hostHwnd) => HostingHwnd = hostHwnd;
177+
178+
public void DebugLog(string message)
179+
{
180+
#if DEBUG
181+
this.ProcessLogMessage(new LogMessage(message));
182+
#endif
183+
}
176184
}

src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs

+51-42
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// The Microsoft Corporation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5-
using System.Collections.ObjectModel;
65
using System.Collections.Specialized;
76
using System.Diagnostics;
87
using CommunityToolkit.Mvvm.Messaging;
@@ -23,8 +22,7 @@ public partial class MainListPage : DynamicListPage,
2322
{
2423
private readonly IServiceProvider _serviceProvider;
2524

26-
private readonly ObservableCollection<TopLevelCommandItemWrapper> _commands;
27-
25+
private readonly TopLevelCommandManager _tlcManager;
2826
private IEnumerable<IListItem>? _filteredItems;
2927

3028
private bool _appsLoading = true;
@@ -36,12 +34,9 @@ public MainListPage(IServiceProvider serviceProvider)
3634
Icon = new(Path.Combine(AppDomain.CurrentDomain.BaseDirectory.ToString(), "Assets\\StoreLogo.scale-200.png"));
3735
_serviceProvider = serviceProvider;
3836

39-
var tlcManager = _serviceProvider.GetService<TopLevelCommandManager>()!;
40-
tlcManager.PropertyChanged += TlcManager_PropertyChanged;
41-
42-
// reference the TLC collection directly... maybe? TODO is this a good idea ot a terrible one?
43-
_commands = tlcManager.TopLevelCommands;
44-
_commands.CollectionChanged += Commands_CollectionChanged;
37+
_tlcManager = _serviceProvider.GetService<TopLevelCommandManager>()!;
38+
_tlcManager.PropertyChanged += TlcManager_PropertyChanged;
39+
_tlcManager.TopLevelCommands.CollectionChanged += Commands_CollectionChanged;
4540

4641
WeakReferenceMessenger.Default.Register<ClearSearchMessage>(this);
4742

@@ -60,7 +55,7 @@ private void TlcManager_PropertyChanged(object? sender, System.ComponentModel.Pr
6055
}
6156
}
6257

63-
private void Commands_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) => RaiseItemsChanged(_commands.Count);
58+
private void Commands_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) => RaiseItemsChanged(_tlcManager.TopLevelCommands.Count);
6459

6560
public override IListItem[] GetItems()
6661
{
@@ -70,9 +65,20 @@ public override IListItem[] GetItems()
7065
_startedAppLoad = true;
7166
}
7267

73-
return string.IsNullOrEmpty(SearchText)
74-
? _commands.Select(tlc => tlc).Where(tlc => !string.IsNullOrEmpty(tlc.Title)).ToArray()
75-
: _filteredItems?.ToArray() ?? [];
68+
if (string.IsNullOrEmpty(SearchText))
69+
{
70+
lock (_tlcManager.TopLevelCommands)
71+
{
72+
return _tlcManager.TopLevelCommands.Select(tlc => tlc).Where(tlc => !string.IsNullOrEmpty(tlc.Title)).ToArray();
73+
}
74+
}
75+
else
76+
{
77+
lock (_tlcManager.TopLevelCommands)
78+
{
79+
return _filteredItems?.ToArray() ?? [];
80+
}
81+
}
7682
}
7783

7884
public override void UpdateSearchText(string oldSearch, string newSearch)
@@ -89,40 +95,43 @@ public override void UpdateSearchText(string oldSearch, string newSearch)
8995
}
9096
}
9197

92-
// This gets called on a background thread, because ListViewModel
93-
// updates the .SearchText of all extensions on a BG thread.
94-
foreach (var command in _commands)
98+
var commands = _tlcManager.TopLevelCommands;
99+
lock (commands)
95100
{
96-
command.TryUpdateFallbackText(newSearch);
97-
}
101+
// This gets called on a background thread, because ListViewModel
102+
// updates the .SearchText of all extensions on a BG thread.
103+
foreach (var command in commands)
104+
{
105+
command.TryUpdateFallbackText(newSearch);
106+
}
98107

99-
// Cleared out the filter text? easy. Reset _filteredItems, and bail out.
100-
if (string.IsNullOrEmpty(newSearch))
101-
{
102-
_filteredItems = null;
103-
RaiseItemsChanged(_commands.Count);
104-
return;
105-
}
108+
// Cleared out the filter text? easy. Reset _filteredItems, and bail out.
109+
if (string.IsNullOrEmpty(newSearch))
110+
{
111+
_filteredItems = null;
112+
RaiseItemsChanged(commands.Count);
113+
return;
114+
}
106115

107-
// If the new string doesn't start with the old string, then we can't
108-
// re-use previous results. Reset _filteredItems, and keep er moving.
109-
if (!newSearch.StartsWith(oldSearch, StringComparison.CurrentCultureIgnoreCase))
110-
{
111-
_filteredItems = null;
112-
}
116+
// If the new string doesn't start with the old string, then we can't
117+
// re-use previous results. Reset _filteredItems, and keep er moving.
118+
if (!newSearch.StartsWith(oldSearch, StringComparison.CurrentCultureIgnoreCase))
119+
{
120+
_filteredItems = null;
121+
}
113122

114-
// If we don't have any previous filter results to work with, start
115-
// with a list of all our commands & apps.
116-
if (_filteredItems == null)
117-
{
118-
IEnumerable<IListItem> commands = _commands;
119-
IEnumerable<IListItem> apps = AllAppsCommandProvider.Page.GetItems();
120-
_filteredItems = commands.Concat(apps);
121-
}
123+
// If we don't have any previous filter results to work with, start
124+
// with a list of all our commands & apps.
125+
if (_filteredItems == null)
126+
{
127+
IEnumerable<IListItem> apps = AllAppsCommandProvider.Page.GetItems();
128+
_filteredItems = commands.Concat(apps);
129+
}
122130

123-
// Produce a list of everything that matches the current filter.
124-
_filteredItems = ListHelpers.FilterList<IListItem>(_filteredItems, SearchText, ScoreTopLevelItem);
125-
RaiseItemsChanged(_filteredItems.Count());
131+
// Produce a list of everything that matches the current filter.
132+
_filteredItems = ListHelpers.FilterList<IListItem>(_filteredItems, SearchText, ScoreTopLevelItem);
133+
RaiseItemsChanged(_filteredItems.Count());
134+
}
126135
}
127136

128137
private bool ActuallyLoading()

0 commit comments

Comments
 (0)