Use the actual devhome code for loading extensions (#8)

As in title.

I'm adding a `ExtensionObject<T>` object too. It doesn't _enforce_ anything. We can use it to wrap up things we get from extensions. You get the object back out with `.Unsafe`, which is a mental clue "this object might not live in this process". It'll at least give us a better clue of all the places where accessing the object might not totally be safe.

Also fixes a bug that makes cmdpal a bit more resilient to an extension dying and being reloaded. Just go to all apps & back, and presto, reload.
This commit is contained in:
Mike Griese
2024-08-29 15:26:55 -05:00
committed by GitHub
parent 1120e673f7
commit 6328ff1160
13 changed files with 791 additions and 262 deletions

View File

@@ -3,24 +3,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using System.Xml.Linq;
using ABI.System;
using Microsoft.UI;
using Microsoft.Windows.CommandPalette.Extensions;
using Microsoft.Windows.CommandPalette.Extensions.Helpers;
using Windows.Foundation;
using Windows.Storage.Streams;
namespace SpongebotExtension;
@@ -98,15 +86,10 @@ public class SpongebotPage : Microsoft.Windows.CommandPalette.Extensions.Helpers
var content = new System.Net.Http.FormUrlEncodedContent(bodyObj);
var resp = await client.PostAsync("https://api.imgflip.com/caption_image", content);
var respBody = await resp.Content.ReadAsStringAsync();
// dynamic r = JsonSerializer.Deserialize(respBody);
var response = JsonNode.Parse(respBody);
// var url = r?.data?.url;
var url = response["data"]?["url"]?.ToString() ?? "";
// var encodedMessage = JsonEncodedText.Encode(Message).ToString();
// var encodedUrl = JsonEncodedText.Encode(url).ToString();
//
var body = $$"""
SpongeBot says:
![{{text}}]({{url}})
@@ -144,19 +127,6 @@ internal sealed class SpongebotCommandsProvider : ICommandProvider
};
return [ listItem ];
}
// public IAsyncOperation<IReadOnlyList<ICommand>> GetCommands()
// {
// var spongeCommand = new SpongeDynamicCommandHost() ;
// var settingsCommand = new SettingsCommand() ;
// ICommand command = (File.Exists(SettingsCommand.StateJsonPath())?
// spongeCommand : settingsCommand);
// var list = new List<ICommand>()
// {
// command
// };
// return Task.FromResult(list as IReadOnlyList<ICommand>).AsAsyncOperation();
// }
}

View File

@@ -18,4 +18,8 @@
<PackageReference Include="Microsoft.WindowsAppSDK" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\extensionsdk\Microsoft.Windows.CommandPalette.Extensions.Helpers\Microsoft.CmdPal.Extensions.Helpers.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,35 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Microsoft.CmdPal.Common.Services;
public interface IExtensionService
{
Task<IEnumerable<IExtensionWrapper>> GetInstalledExtensionsAsync(bool includeDisabledExtensions = false);
// Task<IEnumerable<string>> GetInstalledHomeWidgetPackageFamilyNamesAsync(bool includeDisabledExtensions = false);
Task<IEnumerable<IExtensionWrapper>> GetInstalledExtensionsAsync(Microsoft.Windows.CommandPalette.Extensions.ProviderType providerType, bool includeDisabledExtensions = false);
IExtensionWrapper? GetInstalledExtension(string extensionUniqueId);
Task SignalStopExtensionsAsync();
public event EventHandler OnExtensionsChanged;
public void EnableExtension(string extensionUniqueId);
public void DisableExtension(string extensionUniqueId);
///// <summary>
///// Gets a boolean indicating whether the extension was disabled due to the corresponding Windows optional feature
///// being absent from the machine or in an unknown state.
///// </summary>
///// <param name="extension">The out of proc extension object</param>
///// <returns>True only if the extension was disabled. False otherwise.</returns>
//public Task<bool> DisableExtensionIfWindowsFeatureNotAvailable(IExtensionWrapper extension);
}

View File

@@ -0,0 +1,112 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Windows.CommandPalette.Extensions;
using Windows.ApplicationModel;
namespace Microsoft.CmdPal.Common.Services;
public interface IExtensionWrapper
{
/// <summary>
/// Gets the DisplayName of the package as mentioned in the manifest
/// </summary>
string PackageDisplayName { get; }
/// <summary>
/// Gets DisplayName of the extension as mentioned in the manifest
/// </summary>
string ExtensionDisplayName { get; }
/// <summary>
/// Gets PackageFullName of the extension
/// </summary>
string PackageFullName { get; }
/// <summary>
/// Gets PackageFamilyName of the extension
/// </summary>
string PackageFamilyName { get; }
/// <summary>
/// Gets Publisher of the extension
/// </summary>
string Publisher { get; }
/// <summary>
/// Gets class id (GUID) of the extension class (which implements IExtension) as mentioned in the manifest
/// </summary>
string ExtensionClassId { get; }
/// <summary>
/// Gets the date on which the application package was installed or last updated.
/// </summary>
DateTimeOffset InstalledDate { get; }
/// <summary>
/// Gets the PackageVersion of the extension
/// </summary>
PackageVersion Version { get; }
/// <summary>
/// Gets the Unique Id for the extension
/// </summary>
public string ExtensionUniqueId { get; }
/// <summary>
/// Checks whether we have a reference to the extension process and we are able to call methods on the interface.
/// </summary>
/// <returns>Whether we have a reference to the extension process and we are able to call methods on the interface.</returns>
bool IsRunning();
/// <summary>
/// Starts the extension if not running
/// </summary>
/// <returns>An awaitable task</returns>
Task StartExtensionAsync();
/// <summary>
/// Signals the extension to dispose itself and removes the reference to the extension com object
/// </summary>
void SignalDispose();
/// <summary>
/// Gets the underlying instance of IExtension
/// </summary>
/// <returns>Instance of IExtension</returns>
IExtension? GetExtensionObject();
/// <summary>
/// Tells the wrapper that the extension implements the given provider
/// </summary>
/// <param name="providerType">The type of provider to be added</param>
void AddProviderType(ProviderType providerType);
/// <summary>
/// Checks whether the given provider was added through `AddProviderType` method
/// </summary>
/// <param name="providerType">The type of the provider to be checked for</param>
/// <returns>Whether the given provider was added through `AddProviderType` method</returns>
bool HasProviderType(ProviderType providerType);
/// <summary>
/// Starts the extension if not running and gets the provider from the underlying IExtension object
/// Can be null if not found
/// </summary>
/// <typeparam name="T">The type of provider</typeparam>
/// <returns>Nullable instance of the provider</returns>
Task<T?> GetProviderAsync<T>()
where T : class;
/// <summary>
/// Starts the extension if not running and gets a list of providers of type T from the underlying IExtension object.
/// If no providers are found, returns an empty list.
/// </summary>
/// <typeparam name="T">The type of provider</typeparam>
/// <returns>Nullable instance of the provider</returns>
Task<IEnumerable<T>> GetListOfProvidersAsync<T>()
where T : class;
}

View File

@@ -25,6 +25,7 @@ using Windows.ApplicationModel;
using Windows.ApplicationModel.Activation;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Microsoft.Windows.CommandPalette.Services;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
@@ -38,7 +39,7 @@ public partial class App : Application, IApp
private Window? window;
public Window? AppWindow
{
get { return window; }
get => window;
private set { }
}
@@ -79,6 +80,7 @@ public partial class App : Application, IApp
// Core Services
services.AddSingleton<IFileService, FileService>();
services.AddSingleton<IExtensionService, ExtensionService>();
//// Main window: Allow access to the main window
//// from anywhere in the application.

View File

@@ -1,120 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.ApplicationModel.AppExtensions;
using Windows.ApplicationModel;
using Windows.Foundation.Collections;
namespace DeveloperCommandPalette;
internal sealed class ExtensionLoader
{
private const string CreateInstanceProperty = "CreateInstance";
private const string ClassIdProperty = "@ClassId";
private static IPropertySet? GetSubPropertySet(IPropertySet propSet, string name)
{
return propSet.TryGetValue(name, out var value) ? value as IPropertySet : null;
}
private static object[]? GetSubPropertySetArray(IPropertySet propSet, string name)
{
return propSet.TryGetValue(name, out var value) ? value as object[] : null;
}
private static string? GetProperty(IPropertySet propSet, string name)
{
return propSet[name] as string;
}
/// <summary>
/// There are cases where the extension creates multiple COM instances.
/// </summary>
/// <param name="activationPropSet">Activation property set object</param>
/// <returns>List of ClassId strings associated with the activation property</returns>
private static List<string> GetCreateInstanceList(IPropertySet activationPropSet)
{
var propSetList = new List<string>();
var singlePropertySet = GetSubPropertySet(activationPropSet, CreateInstanceProperty);
if (singlePropertySet != null)
{
var classId = GetProperty(singlePropertySet, ClassIdProperty);
// If the instance has a classId as a single string, then it's only supporting a single instance.
if (classId != null)
{
propSetList.Add(classId);
}
}
else
{
var propertySetArray = GetSubPropertySetArray(activationPropSet, CreateInstanceProperty);
if (propertySetArray != null)
{
foreach (var prop in propertySetArray)
{
if (prop is not IPropertySet propertySet)
{
continue;
}
var classId = GetProperty(propertySet, ClassIdProperty);
if (classId != null)
{
propSetList.Add(classId);
}
}
}
}
return propSetList;
}
public static async Task<(IPropertySet?, List<string>)> GetExtensionPropertiesAsync(AppExtension extension)
{
var classIds = new List<string>();
var properties = await extension.GetExtensionPropertiesAsync();
if (properties is null)
{
return (null, classIds);
}
var CmdPalProvider = GetSubPropertySet(properties, "CmdPalProvider");
if (CmdPalProvider is null)
{
return (null, classIds);
}
var activation = GetSubPropertySet(CmdPalProvider, "Activation");
if (activation is null)
{
return (CmdPalProvider, classIds);
}
// Handle case where extension creates multiple instances.
classIds.AddRange(GetCreateInstanceList(activation));
return (CmdPalProvider, classIds);
}
private static async Task<bool> IsValidExtension(Package package)
{
var extensions = await AppExtensionCatalog.Open("com.microsoft.windows.commandpalette").FindAllAsync();
foreach (var extension in extensions)
{
if (package.Id?.FullName == extension.Package?.Id?.FullName)
{
var (CmdPalProvider, classId) = await GetExtensionPropertiesAsync(extension);
return CmdPalProvider != null && classId.Count != 0;
}
}
return false;
}
}

View File

@@ -5,28 +5,46 @@ using Microsoft.UI.Xaml.Controls;
using Microsoft.Windows.CommandPalette.Extensions;
using System.ComponentModel;
using Microsoft.UI.Dispatching;
using CmdPal.Models;
using System.Runtime.InteropServices;
namespace DeveloperCommandPalette;
public sealed class ListItemViewModel : INotifyPropertyChanged, IDisposable
{
private readonly DispatcherQueue DispatcherQueue;
internal IListItem ListItem { get; init; }
internal ExtensionObject<IListItem> ListItem { get; init; }
internal string Title { get; private set; }
internal string Subtitle { get; private set; }
internal string Icon { get; private set; }
internal Lazy<DetailsViewModel?> _Details;
internal DetailsViewModel? Details => _Details.Value;
internal IFallbackHandler? FallbackHandler => this.ListItem.FallbackHandler;
public event PropertyChangedEventHandler? PropertyChanged;
internal ICommand DefaultAction => ListItem.Command;
internal ICommand? DefaultAction { get {try{ return ListItem.Unsafe.Command;} catch (COMException){return null;}}}
internal bool CanInvoke => DefaultAction != null && DefaultAction is IInvokableCommand or IPage;
internal IconElement IcoElement => Microsoft.Terminal.UI.IconPathConverter.IconMUX(Icon);
private IEnumerable<ICommandContextItem> contextActions => ListItem.MoreCommands == null ? [] : ListItem.MoreCommands.Where(i => i is ICommandContextItem).Select(i=> (ICommandContextItem)i);
private IEnumerable<ICommandContextItem> contextActions
{
get {
try
{
var item = ListItem.Unsafe;
return item.MoreCommands == null ?
[] :
item.MoreCommands.Where(i => i is ICommandContextItem).Select(i => (ICommandContextItem)i);
}
catch (COMException)
{
/* log something */
return [];
}
}
}
internal bool HasMoreCommands => contextActions.Any();
internal TagViewModel[] Tags = [];
@@ -36,16 +54,25 @@ public sealed class ListItemViewModel : INotifyPropertyChanged, IDisposable
{
get
{
var l = contextActions.Select(a => new ContextItemViewModel(a)).ToList();
l.Insert(0, new(DefaultAction));
return l;
try
{
var l = contextActions.Select(a => new ContextItemViewModel(a)).ToList();
var def = DefaultAction;
if (def!=null) l.Insert(0, new(def));
return l;
}
catch (COMException)
{
/* log something */
return [];
}
}
}
public ListItemViewModel(IListItem model)
{
model.PropChanged += ListItem_PropertyChanged;
this.ListItem = model;
this.ListItem = new(model);
this.Title = model.Title;
this.Subtitle = model.Subtitle;
this.Icon = model.Command.Icon.Icon;
@@ -54,41 +81,58 @@ public sealed class ListItemViewModel : INotifyPropertyChanged, IDisposable
this.Tags = model.Tags.Select(t => new TagViewModel(t)).ToArray();
}
this._Details = new(() => model.Details != null ? new(this.ListItem.Details) : null);
this._Details = new(() => {
try
{
var item = this.ListItem.Unsafe;
return item.Details != null ? new(item.Details) : null;
}
catch (COMException)
{
/* log something */
return null;
}
});
this.DispatcherQueue = DispatcherQueue.GetForCurrentThread();
}
private void ListItem_PropertyChanged(object sender, Microsoft.Windows.CommandPalette.Extensions.PropChangedEventArgs args)
{
switch (args.PropertyName)
{
case "Name":
case nameof(Title):
{
this.Title = ListItem.Title;
}
break;
case nameof(Subtitle):
{
this.Subtitle = ListItem.Subtitle;
}
break;
case "MoreCommands":
{
BubbleXamlPropertyChanged(nameof(HasMoreCommands));
BubbleXamlPropertyChanged(nameof(ContextActions));
}
break;
case nameof(Icon):
{
this.Icon = ListItem.Command.Icon.Icon;
BubbleXamlPropertyChanged(nameof(IcoElement));
}
break;
}
BubbleXamlPropertyChanged(args.PropertyName);
try{
var item = ListItem.Unsafe;
switch (args.PropertyName)
{
case "Name":
case nameof(Title):
{
this.Title = item.Title;
}
break;
case nameof(Subtitle):
{
this.Subtitle = item.Subtitle;
}
break;
case "MoreCommands":
{
BubbleXamlPropertyChanged(nameof(HasMoreCommands));
BubbleXamlPropertyChanged(nameof(ContextActions));
}
break;
case nameof(Icon):
{
this.Icon = item.Command.Icon.Icon;
BubbleXamlPropertyChanged(nameof(IcoElement));
}
break;
}
BubbleXamlPropertyChanged(args.PropertyName);
} catch (COMException) {
/* log something */
}
}
private void BubbleXamlPropertyChanged(string propertyName)
@@ -106,7 +150,10 @@ public sealed class ListItemViewModel : INotifyPropertyChanged, IDisposable
public void Dispose()
{
this.ListItem.PropChanged -= ListItem_PropertyChanged;
try{
this.ListItem.Unsafe.PropChanged -= ListItem_PropertyChanged;
} catch (COMException) {
/* log something */
}
}
}

View File

@@ -14,6 +14,7 @@ using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Navigation;
using Microsoft.Windows.CommandPalette.Extensions.Helpers;
using Microsoft.Windows.CommandPalette.Extensions;
using System.Runtime.InteropServices;
namespace DeveloperCommandPalette;
@@ -90,7 +91,7 @@ public sealed class NoOpAction : InvokableCommand
{
public override ICommandResult Invoke() { return ActionResult.KeepOpen(); }
}
public sealed class ErrorListItem : ListItem
public sealed class ErrorListItem : Microsoft.Windows.CommandPalette.Extensions.Helpers.ListItem
{
public ErrorListItem(Exception ex) : base(new NoOpAction()) {
this.Title = "Error in extension:";
@@ -198,9 +199,20 @@ public sealed class ListPageViewModel : PageViewModel
//// TODO! Probably bad that this turns list view models into listitems back to NEW view models
//return ListHelpers.FilterList(Items.Select(vm => vm.ListItem), Query).Select(li => new ListItemViewModel(li)).ToList();
var allFilteredItems = ListHelpers.FilterList(Items.SelectMany(section => section).Select(vm => vm.ListItem), Query).Select(li => new ListItemViewModel(li));
var newSection = new SectionInfoList(null, allFilteredItems);
return [newSection];
try{
var allFilteredItems = ListHelpers.FilterList(
Items
.SelectMany(section => section)
.Select(vm => vm.ListItem.Unsafe),
Query).Select(li => new ListItemViewModel(li)
);
var newSection = new SectionInfoList(null, allFilteredItems);
return [newSection];
}
catch (COMException ex)
{
return [new SectionInfoList(null, [new ListItemViewModel(new ErrorListItem(ex))])];
}
}
}
}

View File

@@ -8,6 +8,7 @@ using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.Windows.CommandPalette.Extensions.Helpers;
using Microsoft.Windows.CommandPalette.Extensions;
using CmdPal.Models;
namespace DeveloperCommandPalette;
@@ -58,7 +59,7 @@ public sealed class RecentsListSection : ListSection, INotifyCollectionChanged
var apps = _mainViewModel.Recent;
foreach (var app in apps)
{
_Items.Add(new MainListItem(app));
_Items.Add(new MainListItem(app.Unsafe)); // we know these are all local
}
}
}
@@ -82,7 +83,7 @@ public sealed class MainListSection : ISection, INotifyCollectionChanged
// * Just the top-level actions (if there's no query)
// * OR the top-level actions AND the apps (if there's a query)
private IEnumerable<IListItem> itemsToEnumerate =>
_Items.Where(i => i != null && (!_mainViewModel.Recent.Contains(i.Item)));
_Items.Where(i => i != null && (!_mainViewModel.IsRecentCommand(i)));
// Watch out future me!
//
@@ -98,7 +99,7 @@ public sealed class MainListSection : ISection, INotifyCollectionChanged
public MainListSection(MainViewModel viewModel)
{
this._mainViewModel = viewModel;
_Items = new(_mainViewModel.TopLevelCommands.Select(a => new MainListItem(a)));
_Items = new(_mainViewModel.TopLevelCommands.Select(w=>w.Unsafe).Where(li=>li!=null).Select(li => new MainListItem(li!)));
_Items.CollectionChanged += Bubble_CollectionChanged; ;
}
@@ -195,7 +196,8 @@ public sealed class FilteredListSection : ISection, INotifyCollectionChanged
public FilteredListSection(MainViewModel viewModel)
{
this._mainViewModel = viewModel;
_Items = new(_mainViewModel.TopLevelCommands.Select(a => new MainListItem(a)));
// TODO: We should probably just get rid of MainListItem entirely, so I'm leaveing these uncaught
_Items = new(_mainViewModel.TopLevelCommands.Where(wrapper=>wrapper.Unsafe!=null).Select(wrapper => new MainListItem(wrapper.Unsafe!)));
_Items.CollectionChanged += Bubble_CollectionChanged; ;
}
@@ -261,19 +263,20 @@ public sealed class MainListPage : Microsoft.Windows.CommandPalette.Extensions.H
if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems != null)
{
foreach (var item in e.NewItems)
if (item is IListItem listItem)
if (item is ExtensionObject<IListItem> listItem)
{
// Eh, it's fine to be unsafe here, we're probably tossing MainListItem
if (!_mainViewModel.Recent.Contains(listItem))
{
_mainSection._Items.Add(new MainListItem(listItem));
_mainSection._Items.Add(new MainListItem(listItem.Unsafe));
}
_filteredSection._Items.Add(new MainListItem(listItem));
_filteredSection._Items.Add(new MainListItem(listItem.Unsafe));
}
}
else if (e.Action == NotifyCollectionChangedAction.Remove && e.OldItems != null)
{
foreach (var item in e.OldItems)
if (item is IListItem listItem)
if (item is ExtensionObject<IListItem> listItem)
{
foreach (var mainListItem in _mainSection._Items) // MainListItem
if (mainListItem.Item == listItem)

View File

@@ -1,16 +1,19 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Collections.ObjectModel;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.WindowsRuntime;
using CmdPal.Models;
using Microsoft.CmdPal.Common.Extensions;
using Microsoft.CmdPal.Common.Services;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.Windows.CommandPalette.Extensions;
using Windows.ApplicationModel.AppExtensions;
using Windows.Foundation;
using System.Collections.ObjectModel;
using Microsoft.UI.Xaml.Media.Animation;
using Microsoft.Windows.CommandPalette.Extensions;
using Microsoft.Windows.CommandPalette.Extensions.Helpers;
using Windows.Foundation;
using Windows.Win32;
namespace DeveloperCommandPalette;
@@ -19,7 +22,7 @@ public sealed class MainViewModel
{
internal readonly AllApps.AllAppsPage apps = new();
internal readonly ObservableCollection<ActionsProviderWrapper> CommandsProviders = new();
internal readonly ObservableCollection<IListItem> TopLevelCommands = [];
internal readonly ObservableCollection<ExtensionObject<IListItem>> TopLevelCommands = [];
internal readonly List<ICommandProvider> _builtInCommands = [];
@@ -28,7 +31,9 @@ public sealed class MainViewModel
internal bool LoadedApps;
public event TypedEventHandler<object, object?>? HideRequested;
public event TypedEventHandler<object, object?>? SummonRequested;
public event TypedEventHandler<object, object?>? AppsReady;
internal MainViewModel()
@@ -47,10 +52,11 @@ public sealed class MainViewModel
AppsReady?.Invoke(this, null);
}).Start();
}
public void ResetTopLevel()
{
TopLevelCommands.Clear();
TopLevelCommands.Add(new ListItem(apps));
TopLevelCommands.Add(new(new ListItem(apps)));
}
internal void RequestHide()
@@ -68,31 +74,72 @@ public sealed class MainViewModel
{
return title + subtitle;
}
private string[] _recentCommandHashes = [];// ["SpotifySpotify", "All Apps", "GitHub Issues", "Microsoft/GithubBookmark"];
public IEnumerable<IListItem> RecentActions => TopLevelCommands.Where(i => i != null && _recentCommandHashes.Contains(CreateHash(i.Title, i.Subtitle)));
public IEnumerable<IListItem> RecentActions => TopLevelCommands
.Select(i=>i.Unsafe)
.Where((i) => {
if (i != null)
{
try{
return _recentCommandHashes.Contains(CreateHash(i.Title, i.Subtitle));
} catch(COMException){ return false; }
}
return false;
})
.Select(i=>i!);
public IEnumerable<IListItem> AppItems => LoadedApps? apps.GetItems().First().Items : [];
public IEnumerable<IListItem> Everything => TopLevelCommands.Concat(AppItems).Where(i => i!= null);
public IEnumerable<IListItem> Recent => _recentCommandHashes.Select(hash => Everything.Where(i => CreateHash(i.Title, i.Subtitle) == hash ).FirstOrDefault()).Where(i => i != null).Select(i=>i!);
public IEnumerable<ExtensionObject<IListItem>> Everything => TopLevelCommands
.Concat(AppItems.Select(i => new ExtensionObject<IListItem>(i)))
.Where(i => i!= null);
public IEnumerable<ExtensionObject<IListItem>> Recent => _recentCommandHashes
.Select(hash =>
Everything
.Where(i => {
try {
var o = i.Unsafe;
return CreateHash(o.Title, o.Subtitle) == hash;
} catch (COMException) { return false; }
})
.FirstOrDefault()
)
.Where(i => i != null)
.Select(i=>i!);
public bool IsRecentCommand(MainListItem item)
{
try
{
foreach (var wraprer in Recent)
{
if (wraprer.Unsafe == item) return true;
}
}
catch (COMException) { return false; }
return false;
}
internal void PushRecentAction(ICommand action)
{
IEnumerable<IListItem> topLevel = TopLevelCommands;
if (LoadedApps)
foreach (var wrapped in Everything)
{
topLevel = topLevel.Concat(AppItems);
}
foreach (var listItem in topLevel)
{
if (listItem != null && listItem.Command == action)
{
// Found it, awesome.
var hash = CreateHash(listItem.Title, listItem.Subtitle);
// Remove the old one and push the new one to the front
var recent = new List<string>([hash]).Concat(_recentCommandHashes.Where(h => h != hash)).Take(5).ToArray();
_recentCommandHashes = recent.ToArray();
return;
try{
var listItem = wrapped?.Unsafe;
if (listItem != null && listItem.Command == action)
{
// Found it, awesome.
var hash = CreateHash(listItem.Title, listItem.Subtitle);
// Remove the old one and push the new one to the front
var recent = new List<string>([hash]).Concat(_recentCommandHashes.Where(h => h != hash)).Take(5).ToArray();
_recentCommandHashes = recent.ToArray();
return;
}
}
catch(COMException){ /* log something */ }
}
}
}
@@ -103,6 +150,7 @@ public sealed class MainViewModel
public sealed partial class MainPage : Page
{
private string _log = "";
public MainViewModel ViewModel { get; } = new MainViewModel();
public MainPage()
@@ -112,15 +160,26 @@ public sealed partial class MainPage : Page
var rootListVm = new ListPageViewModel(new MainListPage(ViewModel));
InitializePage(rootListVm);
// TODO! make this async: it was originally on Page_Loaded and was async from there
// LoadAllCommands().Wait();
LoadBuiltinCommandsAsync().Wait();
var extensionService = Application.Current.GetService<IExtensionService>();
if (extensionService != null)
{
extensionService.OnExtensionsChanged += ExtensionService_OnExtensionsChanged;
}
_ = LoadExtensions();
RootFrame.Navigate(typeof(ListPage), rootListVm, new DrillInNavigationTransitionInfo());
}
private void ExtensionService_OnExtensionsChanged(object? sender, EventArgs e)
{
_ = LoadAllCommands();
}
private void _HackyBadClearFilter()
{
// BODGY but I don't care, cause i'm throwing this all out
@@ -129,7 +188,6 @@ public sealed partial class MainPage : Page
tb.Focus(FocusState.Programmatic);
}
_ = LoadAllCommands();
}
private void ViewModel_SummonRequested(object sender, object? args)
@@ -139,9 +197,11 @@ public sealed partial class MainPage : Page
_HackyBadClearFilter();
}
}
private void Page_Loaded(object sender, RoutedEventArgs e)
{
}
private async Task LoadAllCommands()
{
ViewModel.ResetTopLevel();
@@ -151,6 +211,7 @@ public sealed partial class MainPage : Page
_ = LoadExtensions();
}
public async Task LoadBuiltinCommandsAsync()
{
// Load commands from builtins
@@ -208,10 +269,19 @@ public sealed partial class MainPage : Page
if (!provider.IsExtension) continue;
foreach (var item in provider.TopLevelItems)
{
if (action == item.Command)
// TODO! We really need a better "SafeWrapper<T>" object that can make sure
// that an extension object is alive when we call things on it.
// Case in point: this. If the extension was killed while we're open, then
// COM calls on it crash (and then we just do nothing)
try
{
provider.AllowSetForeground(true);
if (action == item.Command)
{
provider.AllowSetForeground(true);
return;
}
}
catch (COMException e){ AppendLog(e.Message); }
}
}
}
@@ -278,31 +348,22 @@ public sealed partial class MainPage : Page
private async Task LoadExtensions()
{
if (ViewModel != null) ViewModel.LoadingExtensions = true;
// Get extensions for us:
AppExtensionCatalog extensionCatalog = AppExtensionCatalog.Open("com.microsoft.windows.commandpalette");
IReadOnlyList<AppExtension> extensions = await extensionCatalog.FindAllAsync();
foreach (var extension in extensions)
if (ViewModel == null) return;
ViewModel.LoadingExtensions = true;
var extnService = Application.Current.GetService<IExtensionService>();
if (extnService != null)
{
var name = extension.DisplayName;
var id = extension.Id;
var pfn = extension.Package.Id.FamilyName;
var (provider, classIds) = await ExtensionLoader.GetExtensionPropertiesAsync(extension);
if (provider == null || classIds.Count == 0)
var extensions = await extnService.GetInstalledExtensionsAsync(ProviderType.Commands, includeDisabledExtensions: false);
foreach (var extension in extensions)
{
continue;
}
AppendLog($"Found Extension:{name}, {id}, {pfn}->");
foreach (var classId in classIds)
{
_ = LoadExtensionClassObject(extension, classId);
if (extension == null) continue;
await LoadActionExtensionObject(extension);
}
}
if (ViewModel != null)
{
ViewModel.LoadingExtensions = false;
@@ -310,16 +371,13 @@ public sealed partial class MainPage : Page
}
}
private async Task LoadExtensionClassObject(AppExtension extension, string classId)
private async Task LoadActionExtensionObject(IExtensionWrapper extension)
{
AppendLog($"\t{classId}");
try
{
var extensionWrapper = new ExtensionWrapper(extension, classId);
await extensionWrapper.StartExtensionAsync();
var wrapper = new ActionsProviderWrapper(extensionWrapper);
await extension.StartExtensionAsync();
var wrapper = new ActionsProviderWrapper(extension);
ViewModel.CommandsProviders.Add(wrapper);
await LoadTopLevelCommandsFromProvider(wrapper);
}
catch (Exception ex)
@@ -331,11 +389,10 @@ public sealed partial class MainPage : Page
private async Task LoadTopLevelCommandsFromProvider(ActionsProviderWrapper actionProvider)
{
// TODO! do this better async
await actionProvider.LoadTopLevelCommands().ConfigureAwait(false);
foreach (var i in actionProvider.TopLevelItems)
{
ViewModel.TopLevelCommands.Add(i);
ViewModel.TopLevelCommands.Add(new(i));
}
}
@@ -386,7 +443,7 @@ sealed class ActionsProviderWrapper
public bool IsExtension => extensionWrapper != null;
private readonly bool isValid;
private ICommandProvider ActionProvider { get; }
private readonly ExtensionWrapper? extensionWrapper;
private readonly IExtensionWrapper? extensionWrapper;
private IListItem[] _topLevelItems = [];
public IListItem[] TopLevelItems => _topLevelItems;
@@ -394,7 +451,7 @@ sealed class ActionsProviderWrapper
ActionProvider = provider;
isValid = true;
}
public ActionsProviderWrapper(ExtensionWrapper extension)
public ActionsProviderWrapper(IExtensionWrapper extension)
{
extensionWrapper = extension;
var extensionImpl = extension.GetExtensionObject();
@@ -416,8 +473,16 @@ sealed class ActionsProviderWrapper
{
_topLevelItems = commands;
}
}
// public async Task<bool> Ping()
// {
// if (!isValid) return false;
// if (extensionWrapper != null)
// {
// return extensionWrapper.IsRunning();
// }
// return false;
// }
public void AllowSetForeground(bool allow)
{

View File

@@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Runtime.InteropServices;
using Microsoft.CmdPal.Common.Services;
using Microsoft.Windows.CommandPalette.Extensions;
using Windows.ApplicationModel;
using Windows.ApplicationModel.AppExtensions;
using Windows.Win32;
using Windows.Win32.System.Com;
using WinRT;
namespace CmdPal.Models;
public class ExtensionObject<T> // where T : IInspectable
{
private readonly T _value;
public ExtensionObject(T value)
{
_value = value;
}
// public T? Safe {
// get {
// try {
// if (_value!.Equals(_value)) return _value;
// } catch (COMException){ /* log something */ }
// return default;
// }
// }
public T Unsafe => _value;
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT License.
using System.Runtime.InteropServices;
using Microsoft.CmdPal.Common.Services;
using Microsoft.Windows.CommandPalette.Extensions;
using Windows.ApplicationModel;
using Windows.ApplicationModel.AppExtensions;
@@ -9,9 +10,9 @@ using Windows.Win32;
using Windows.Win32.System.Com;
using WinRT;
namespace DeveloperCommandPalette;
namespace CmdPal.Models;
public class ExtensionWrapper
public class ExtensionWrapper : IExtensionWrapper
{
private const int HResultRpcServerNotRunning = -2147023174;

View File

@@ -0,0 +1,365 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using CmdPal.Models;
using Microsoft.CmdPal.Common.Contracts;
using Microsoft.CmdPal.Common.Extensions;
using Microsoft.CmdPal.Common.Services;
using Microsoft.UI.Xaml;
using Microsoft.Windows.CommandPalette.Extensions;
using Windows.ApplicationModel;
using Windows.ApplicationModel.AppExtensions;
using Windows.Foundation.Collections;
namespace Microsoft.Windows.CommandPalette.Services;
public class ExtensionService : IExtensionService, IDisposable
{
// private readonly ILogger _log = Log.ForContext("SourceContext", nameof(ExtensionService));
public event EventHandler OnExtensionsChanged = (_, _) => { };
private static readonly PackageCatalog _catalog = PackageCatalog.OpenForCurrentUser();
private static readonly object _lock = new();
private readonly SemaphoreSlim _getInstalledExtensionsLock = new(1, 1);
private readonly SemaphoreSlim _getInstalledWidgetsLock = new(1, 1);
private readonly ILocalSettingsService _localSettingsService;
private bool _disposedValue;
private const string CreateInstanceProperty = "CreateInstance";
private const string ClassIdProperty = "@ClassId";
private static readonly List<IExtensionWrapper> _installedExtensions = new();
private static readonly List<IExtensionWrapper> _enabledExtensions = new();
public ExtensionService(ILocalSettingsService settingsService)
{
_catalog.PackageInstalling += Catalog_PackageInstalling;
_catalog.PackageUninstalling += Catalog_PackageUninstalling;
_catalog.PackageUpdating += Catalog_PackageUpdating;
// These two were an investigation into getting updates when a package
// gets redeployed from VS. Neither get raised (nor do the above)
// _catalog.PackageStatusChanged += Catalog_PackageStatusChanged;
// _catalog.PackageStaging += Catalog_PackageStaging;
_localSettingsService = settingsService;
}
private void Catalog_PackageInstalling(PackageCatalog sender, PackageInstallingEventArgs args)
{
if (args.IsComplete)
{
lock (_lock)
{
var isCmdPalExtension = Task.Run(() =>
{
return IsValidCmdPalExtension(args.Package);
}).Result;
if (isCmdPalExtension)
{
OnPackageChange(args.Package);
}
}
}
}
private void Catalog_PackageUninstalling(PackageCatalog sender, PackageUninstallingEventArgs args)
{
if (args.IsComplete)
{
lock (_lock)
{
foreach (var extension in _installedExtensions)
{
if (extension.PackageFullName == args.Package.Id.FullName)
{
OnPackageChange(args.Package);
break;
}
}
}
}
}
private void Catalog_PackageUpdating(PackageCatalog sender, PackageUpdatingEventArgs args)
{
if (args.IsComplete)
{
lock (_lock)
{
var isCmdPalExtension = Task.Run(() =>
{
return IsValidCmdPalExtension(args.TargetPackage);
}).Result;
if (isCmdPalExtension)
{
OnPackageChange(args.TargetPackage);
}
}
}
}
private void OnPackageChange(Package package)
{
_installedExtensions.Clear();
_enabledExtensions.Clear();
OnExtensionsChanged.Invoke(this, EventArgs.Empty);
}
private static async Task<bool> IsValidCmdPalExtension(Package package)
{
var extensions = await AppExtensionCatalog.Open("com.microsoft.windows.commandpalette").FindAllAsync();
foreach (var extension in extensions)
{
if (package.Id?.FullName == extension.Package?.Id?.FullName)
{
var (CmdPalProvider, classId) = await GetCmdPalExtensionPropertiesAsync(extension);
return CmdPalProvider != null && classId.Count != 0;
}
}
return false;
}
private static async Task<(IPropertySet?, List<string>)> GetCmdPalExtensionPropertiesAsync(AppExtension extension)
{
var classIds = new List<string>();
var properties = await extension.GetExtensionPropertiesAsync();
if (properties is null)
{
return (null, classIds);
}
var CmdPalProvider = GetSubPropertySet(properties, "CmdPalProvider");
if (CmdPalProvider is null)
{
return (null, classIds);
}
var activation = GetSubPropertySet(CmdPalProvider, "Activation");
if (activation is null)
{
return (CmdPalProvider, classIds);
}
// Handle case where extension creates multiple instances.
classIds.AddRange(GetCreateInstanceList(activation));
return (CmdPalProvider, classIds);
}
private static async Task<IEnumerable<AppExtension>> GetInstalledAppExtensionsAsync()
{
return await AppExtensionCatalog.Open("com.microsoft.windows.commandpalette").FindAllAsync();
}
public async Task<IEnumerable<IExtensionWrapper>> GetInstalledExtensionsAsync(bool includeDisabledExtensions = false)
{
await _getInstalledExtensionsLock.WaitAsync();
try
{
if (_installedExtensions.Count == 0)
{
var extensions = await GetInstalledAppExtensionsAsync();
foreach (var extension in extensions)
{
var (CmdPalProvider, classIds) = await GetCmdPalExtensionPropertiesAsync(extension);
if (CmdPalProvider == null || classIds.Count == 0)
{
continue;
}
foreach (var classId in classIds)
{
var extensionWrapper = new ExtensionWrapper(extension, classId);
var supportedInterfaces = GetSubPropertySet(CmdPalProvider, "SupportedInterfaces");
if (supportedInterfaces is not null)
{
foreach (var supportedInterface in supportedInterfaces)
{
ProviderType pt;
if (Enum.TryParse<ProviderType>(supportedInterface.Key, out pt))
{
extensionWrapper.AddProviderType(pt);
}
else
{
// TODO: throw warning or fire notification that extension declared unsupported extension interface
// https://github.com/microsoft/DevHome/issues/617
}
}
}
var localSettingsService = Application.Current.GetService<ILocalSettingsService>();
var extensionUniqueId = extension.AppInfo.AppUserModelId + "!" + extension.Id;
var isExtensionDisabled = await localSettingsService.ReadSettingAsync<bool>(extensionUniqueId + "-ExtensionDisabled");
_installedExtensions.Add(extensionWrapper);
if (!isExtensionDisabled)
{
_enabledExtensions.Add(extensionWrapper);
}
//TelemetryFactory.Get<ITelemetry>().Log(
// "Extension_ReportInstalled",
// LogLevel.Critical,
// new ReportInstalledExtensionEvent(extensionUniqueId, isEnabled: !isExtensionDisabled));
}
}
}
return includeDisabledExtensions ? _installedExtensions : _enabledExtensions;
}
finally
{
_getInstalledExtensionsLock.Release();
}
}
public IExtensionWrapper? GetInstalledExtension(string extensionUniqueId)
{
var extension = _installedExtensions.Where(extension => extension.ExtensionUniqueId.Equals(extensionUniqueId, StringComparison.Ordinal));
return extension.FirstOrDefault();
}
public async Task SignalStopExtensionsAsync()
{
var installedExtensions = await GetInstalledExtensionsAsync();
foreach (var installedExtension in installedExtensions)
{
if (installedExtension.IsRunning())
{
installedExtension.SignalDispose();
}
}
}
public async Task<IEnumerable<IExtensionWrapper>> GetInstalledExtensionsAsync(ProviderType providerType, bool includeDisabledExtensions = false)
{
var installedExtensions = await GetInstalledExtensionsAsync(includeDisabledExtensions);
List<IExtensionWrapper> filteredExtensions = new();
foreach (var installedExtension in installedExtensions)
{
if (installedExtension.HasProviderType(providerType))
{
filteredExtensions.Add(installedExtension);
}
}
return filteredExtensions;
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
_getInstalledExtensionsLock.Dispose();
_getInstalledWidgetsLock.Dispose();
}
_disposedValue = true;
}
}
private static IPropertySet? GetSubPropertySet(IPropertySet propSet, string name)
{
return propSet.TryGetValue(name, out var value) ? value as IPropertySet : null;
}
private static object[]? GetSubPropertySetArray(IPropertySet propSet, string name)
{
return propSet.TryGetValue(name, out var value) ? value as object[] : null;
}
/// <summary>
/// There are cases where the extension creates multiple COM instances.
/// </summary>
/// <param name="activationPropSet">Activation property set object</param>
/// <returns>List of ClassId strings associated with the activation property</returns>
private static List<string> GetCreateInstanceList(IPropertySet activationPropSet)
{
var propSetList = new List<string>();
var singlePropertySet = GetSubPropertySet(activationPropSet, CreateInstanceProperty);
if (singlePropertySet != null)
{
var classId = GetProperty(singlePropertySet, ClassIdProperty);
// If the instance has a classId as a single string, then it's only supporting a single instance.
if (classId != null)
{
propSetList.Add(classId);
}
}
else
{
var propertySetArray = GetSubPropertySetArray(activationPropSet, CreateInstanceProperty);
if (propertySetArray != null)
{
foreach (var prop in propertySetArray)
{
if (prop is not IPropertySet propertySet)
{
continue;
}
var classId = GetProperty(propertySet, ClassIdProperty);
if (classId != null)
{
propSetList.Add(classId);
}
}
}
}
return propSetList;
}
private static string? GetProperty(IPropertySet propSet, string name)
{
return propSet[name] as string;
}
public void EnableExtension(string extensionUniqueId)
{
var extension = _installedExtensions.Where(extension => extension.ExtensionUniqueId.Equals(extensionUniqueId, StringComparison.Ordinal));
_enabledExtensions.Add(extension.First());
}
public void DisableExtension(string extensionUniqueId)
{
var extension = _enabledExtensions.Where(extension => extension.ExtensionUniqueId.Equals(extensionUniqueId, StringComparison.Ordinal));
_enabledExtensions.Remove(extension.First());
}
///// <inheritdoc cref="IExtensionService.DisableExtensionIfWindowsFeatureNotAvailable(IExtensionWrapper)"/>
//public async Task<bool> DisableExtensionIfWindowsFeatureNotAvailable(IExtensionWrapper extension)
//{
// // Only attempt to disable feature if its available.
// if (IsWindowsOptionalFeatureAvailableForExtension(extension.ExtensionClassId))
// {
// return false;
// }
// _log.Warning($"Disabling extension: '{extension.ExtensionDisplayName}' because its feature is absent or unknown");
// // Remove extension from list of enabled extensions to prevent Dev Home from re-querying for this extension
// // for the rest of its process lifetime.
// DisableExtension(extension.ExtensionUniqueId);
// // Update the local settings so the next time the user launches Dev Home the extension will be disabled.
// await _localSettingsService.SaveSettingAsync(extension.ExtensionUniqueId + "-ExtensionDisabled", true);
// return true;
//}
}