Working on DI

This commit is contained in:
Michael Jolley
2026-01-29 21:42:53 -06:00
parent 303be86d86
commit fa4bc0a397
14 changed files with 439 additions and 843 deletions

View File

@@ -1,37 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Threading.Tasks;
using Windows.Foundation;
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.CommandPalette.Extensions.ProviderType providerType, bool includeDisabledExtensions = false);
IExtensionWrapper? GetInstalledExtension(string extensionUniqueId);
Task SignalStopExtensionsAsync();
event TypedEventHandler<IExtensionService, IEnumerable<IExtensionWrapper>>? OnExtensionAdded;
event TypedEventHandler<IExtensionService, IEnumerable<IExtensionWrapper>>? OnExtensionRemoved;
void EnableExtension(string extensionUniqueId);
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

@@ -2,7 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CommandPalette.Extensions;
using Microsoft.Extensions.Logging;

View File

@@ -2,8 +2,8 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CommandPalette.Extensions;
using Microsoft.Extensions.Logging;

View File

@@ -0,0 +1,295 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CommandPalette.Extensions;
using Microsoft.Extensions.Logging;
using Windows.Foundation;
namespace Microsoft.CmdPal.UI.ViewModels.Models;
public partial class BuiltInExtensionService : IExtensionService, IDisposable
{
public event TypedEventHandler<CommandProviderWrapper, IEnumerable<CommandProviderWrapper>>? OnCommandProviderAdded;
public event TypedEventHandler<CommandProviderWrapper, IEnumerable<CommandProviderWrapper>>? OnCommandProviderRemoved;
public event TypedEventHandler<CommandProviderWrapper, IEnumerable<TopLevelViewModel>>? OnCommandsAdded;
public event TypedEventHandler<CommandProviderWrapper, IEnumerable<TopLevelViewModel>>? OnCommandsRemoved;
private readonly ILogger _logger;
private readonly TaskScheduler _taskScheduler;
private readonly HotkeyManager _hotkeyManager;
private readonly AliasManager _aliasManager;
private readonly SettingsService _settingsService;
private readonly IEnumerable<ICommandProvider> _commandProviders;
private readonly Lock _commandProvidersLock = new();
private readonly List<CommandProviderWrapper> _builtInCommandWrappers = [];
private readonly SemaphoreSlim _getBuiltInCommandWrappersLock = new(1, 1);
private readonly List<CommandProviderWrapper> _enabledBuiltInCommandWrappers = [];
private readonly SemaphoreSlim _getEnabledBuiltInCommandWrappersLock = new(1, 1);
private readonly List<TopLevelViewModel> _topLevelCommands = [];
private readonly SemaphoreSlim _getTopLevelCommandsLock = new(1, 1);
private WeakReference<IPageContext>? _weakPageContext;
private bool isLoaded;
public BuiltInExtensionService(
IEnumerable<ICommandProvider> commandProviders,
TaskScheduler taskScheduler,
HotkeyManager hotkeyManager,
AliasManager aliasManager,
SettingsService settingsService,
ILogger logger)
{
_logger = logger;
_taskScheduler = taskScheduler;
_hotkeyManager = hotkeyManager;
_aliasManager = aliasManager;
_settingsService = settingsService;
_commandProviders = commandProviders;
}
public async Task<IEnumerable<CommandProviderWrapper>> GetCommandProviderWrappersAsync(WeakReference<IPageContext> weakPageContext, bool includeDisabledExtensions = false)
{
_weakPageContext = weakPageContext;
if (!isLoaded)
{
await LoadBuiltInsAsync();
}
if (includeDisabledExtensions)
{
await _getBuiltInCommandWrappersLock.WaitAsync();
try
{
return _builtInCommandWrappers;
}
finally
{
_getBuiltInCommandWrappersLock.Release();
}
}
else
{
await _getEnabledBuiltInCommandWrappersLock.WaitAsync();
try
{
return _enabledBuiltInCommandWrappers;
}
finally
{
_getEnabledBuiltInCommandWrappersLock.Release();
}
}
}
public async Task<IEnumerable<TopLevelViewModel>> GetTopLevelCommandsAsync()
{
await _getTopLevelCommandsLock.WaitAsync();
try
{
return _topLevelCommands;
}
finally
{
_getTopLevelCommandsLock.Release();
}
}
public async Task SignalStopExtensionsAsync()
{
// We're buil-in. There's no stopping us.
}
public async Task EnableProviderAsync(string providerId)
{
await _getEnabledBuiltInCommandWrappersLock.WaitAsync();
try
{
if (_enabledBuiltInCommandWrappers.Any(wrapper => wrapper.Id.Equals(providerId, StringComparison.Ordinal)))
{
return;
}
}
finally
{
_getEnabledBuiltInCommandWrappersLock.Release();
}
await _getBuiltInCommandWrappersLock.WaitAsync();
try
{
CommandProviderWrapper? wrapper = _builtInCommandWrappers.FirstOrDefault(wrapper => wrapper.Id.Equals(providerId, StringComparison.Ordinal));
if (wrapper != null)
{
await _getEnabledBuiltInCommandWrappersLock.WaitAsync();
try
{
_enabledBuiltInCommandWrappers.Add(wrapper);
}
finally
{
_getEnabledBuiltInCommandWrappersLock.Release();
}
var commands = await LoadTopLevelCommandsFromProvider(wrapper);
await _getTopLevelCommandsLock.WaitAsync();
try
{
foreach (var c in commands)
{
_topLevelCommands.Add(c);
}
}
finally
{
_getTopLevelCommandsLock.Release();
}
OnCommandsAdded?.Invoke(wrapper, commands);
}
}
finally
{
_getBuiltInCommandWrappersLock.Release();
}
}
public async Task DisableProviderAsync(string providerId)
{
await _getEnabledBuiltInCommandWrappersLock.WaitAsync();
try
{
var wrapper = _enabledBuiltInCommandWrappers.FirstOrDefault(wrapper => wrapper.Id.Equals(providerId, StringComparison.Ordinal));
if (wrapper != null)
{
_enabledBuiltInCommandWrappers.Remove(wrapper);
await _getTopLevelCommandsLock.WaitAsync();
try
{
var commands = _topLevelCommands.Where(command => command.CommandProviderId.Equals(wrapper.Id, StringComparison.Ordinal));
foreach (var c in commands)
{
_topLevelCommands.Remove(c);
}
OnCommandsRemoved?.Invoke(wrapper, commands);
}
finally
{
_getTopLevelCommandsLock.Release();
}
}
}
finally
{
_getEnabledBuiltInCommandWrappersLock.Release();
}
}
private async Task LoadBuiltInsAsync()
{
var s = new Stopwatch();
s.Start();
var builtInProviders = _commandProviders;
foreach (var provider in builtInProviders)
{
CommandProviderWrapper wrapper = new(provider, _taskScheduler, _hotkeyManager, _aliasManager, _logger);
lock (_getBuiltInCommandWrappersLock)
{
_builtInCommandWrappers.Add(wrapper);
}
if (wrapper.IsActive)
{
lock (_getEnabledBuiltInCommandWrappersLock)
{
_enabledBuiltInCommandWrappers.Add(wrapper);
}
var commands = await LoadTopLevelCommandsFromProvider(wrapper);
lock (_getTopLevelCommandsLock)
{
foreach (var c in commands)
{
_topLevelCommands.Add(c);
}
}
}
}
s.Stop();
isLoaded = true;
Log_LoadingBuiltInsTook(s.ElapsedMilliseconds);
}
private async Task<IEnumerable<TopLevelViewModel>> LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider)
{
await commandProvider.LoadTopLevelCommands(_settingsService, _weakPageContext!);
var commands = await Task.Factory.StartNew(
() =>
{
List<TopLevelViewModel> commands = [];
foreach (var item in commandProvider.TopLevelItems)
{
commands.Add(item);
}
foreach (var item in commandProvider.FallbackItems)
{
if (item.IsEnabled)
{
commands.Add(item);
}
}
return commands;
},
CancellationToken.None,
TaskCreationOptions.None,
_taskScheduler);
commandProvider.CommandsChanged -= CommandProvider_CommandsChanged;
commandProvider.CommandsChanged += CommandProvider_CommandsChanged;
return commands;
}
private void CommandProvider_CommandsChanged(CommandProviderWrapper commandProviderWrapper, IItemsChangedEventArgs args)
{
}
public void Dispose()
{
_getBuiltInCommandWrappersLock.Dispose();
_getEnabledBuiltInCommandWrappersLock.Dispose();
_getTopLevelCommandsLock.Dispose();
GC.SuppressFinalize(this);
}
private void Test()
{
OnCommandProviderAdded?.Invoke(null!, null!);
OnCommandProviderRemoved?.Invoke(null!, null!);
}
[LoggerMessage(Level = LogLevel.Debug, Message = "Loading built-ins took {elapsedMs}ms")]
partial void Log_LoadingBuiltInsTook(long elapsedMs);
}

View File

@@ -1,452 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Common.Services;
using Microsoft.CommandPalette.Extensions;
using Microsoft.Extensions.Logging;
using Windows.ApplicationModel;
using Windows.ApplicationModel.AppExtensions;
using Windows.Foundation;
using Windows.Foundation.Collections;
namespace Microsoft.CmdPal.UI.ViewModels.Models;
public partial class ExtensionService : IExtensionService, IDisposable
{
public event TypedEventHandler<IExtensionService, IEnumerable<IExtensionWrapper>>? OnExtensionAdded;
public event TypedEventHandler<IExtensionService, IEnumerable<IExtensionWrapper>>? OnExtensionRemoved;
private static readonly PackageCatalog _catalog = PackageCatalog.OpenForCurrentUser();
private static readonly Lock _lock = new();
private readonly SemaphoreSlim _getInstalledExtensionsLock = new(1, 1);
private readonly SemaphoreSlim _getInstalledWidgetsLock = new(1, 1);
private readonly ILogger _logger;
// private readonly ILocalSettingsService _localSettingsService;
private bool _disposedValue;
private const string CreateInstanceProperty = "CreateInstance";
private const string ClassIdProperty = "@ClassId";
private static readonly List<IExtensionWrapper> _installedExtensions = [];
private static readonly List<IExtensionWrapper> _enabledExtensions = [];
public ExtensionService(ILogger logger)
{
_logger = logger;
_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)
{
InstallPackageUnderLock(args.Package);
}
}
}
private void Catalog_PackageUninstalling(PackageCatalog sender, PackageUninstallingEventArgs args)
{
if (args.IsComplete)
{
lock (_lock)
{
UninstallPackageUnderLock(args.Package);
}
}
}
private void Catalog_PackageUpdating(PackageCatalog sender, PackageUpdatingEventArgs args)
{
if (args.IsComplete)
{
lock (_lock)
{
// Get any extension providers that we previously had from this app
UninstallPackageUnderLock(args.TargetPackage);
// then add the new ones.
InstallPackageUnderLock(args.TargetPackage);
}
}
}
private void InstallPackageUnderLock(Package package)
{
var isCmdPalExtensionResult = Task.Run(() =>
{
return IsValidCmdPalExtension(package);
}).Result;
var isExtension = isCmdPalExtensionResult.IsExtension;
var extension = isCmdPalExtensionResult.Extension;
if (isExtension && extension is not null)
{
Log_ExtensionInstalled(extension.DisplayName);
Task.Run(async () =>
{
await _getInstalledExtensionsLock.WaitAsync();
try
{
var wrappers = await CreateWrappersForExtension(extension);
UpdateExtensionsListsFromWrappers(wrappers);
OnExtensionAdded?.Invoke(this, wrappers);
}
finally
{
_getInstalledExtensionsLock.Release();
}
});
}
}
private void UninstallPackageUnderLock(Package package)
{
List<IExtensionWrapper> removedExtensions = [];
foreach (var extension in _installedExtensions)
{
if (extension.PackageFullName == package.Id.FullName)
{
Log_ExtensionUninstalled(extension.PackageDisplayName);
removedExtensions.Add(extension);
}
}
Task.Run(async () =>
{
await _getInstalledExtensionsLock.WaitAsync();
try
{
_installedExtensions.RemoveAll(i => removedExtensions.Contains(i));
_enabledExtensions.RemoveAll(i => removedExtensions.Contains(i));
OnExtensionRemoved?.Invoke(this, removedExtensions);
}
finally
{
_getInstalledExtensionsLock.Release();
}
});
}
private static async Task<IsExtensionResult> IsValidCmdPalExtension(Package package)
{
var extensions = await AppExtensionCatalog.Open("com.microsoft.commandpalette").FindAllAsync();
foreach (var extension in extensions)
{
if (package.Id?.FullName == extension.Package?.Id?.FullName)
{
var (cmdPalProvider, classId) = await GetCmdPalExtensionPropertiesAsync(extension);
return new(cmdPalProvider is not null && classId.Count != 0, extension);
}
}
return new(false, null);
}
private static async Task<(IPropertySet? CmdPalProvider, List<string> ClassIds)> 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() => await AppExtensionCatalog.Open("com.microsoft.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 wrappers = await CreateWrappersForExtension(extension);
UpdateExtensionsListsFromWrappers(wrappers);
}
}
return includeDisabledExtensions ? _installedExtensions : _enabledExtensions;
}
finally
{
_getInstalledExtensionsLock.Release();
}
}
private static void UpdateExtensionsListsFromWrappers(List<ExtensionWrapper> wrappers)
{
foreach (var extensionWrapper in wrappers)
{
// var localSettingsService = Application.Current.GetService<ILocalSettingsService>();
var extensionUniqueId = extensionWrapper.ExtensionUniqueId;
var isExtensionDisabled = false; // 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));
}
}
private async Task<List<ExtensionWrapper>> CreateWrappersForExtension(AppExtension extension)
{
var (cmdPalProvider, classIds) = await GetCmdPalExtensionPropertiesAsync(extension);
if (cmdPalProvider is null || classIds.Count == 0)
{
return [];
}
List<ExtensionWrapper> wrappers = [];
foreach (var classId in classIds)
{
var extensionWrapper = CreateExtensionWrapper(extension, cmdPalProvider, classId);
wrappers.Add(extensionWrapper);
}
return wrappers;
}
private ExtensionWrapper CreateExtensionWrapper(AppExtension extension, IPropertySet cmdPalProvider, string classId)
{
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(supportedInterface.Key, out pt))
{
extensionWrapper.AddProviderType(pt);
}
else
{
// log warning that extension declared unsupported extension interface
Log_UndeclaredInterfaceInExtension(extension.DisplayName, supportedInterface.Key);
}
}
}
return extensionWrapper;
}
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)
{
Log_DisposingExtension(installedExtension.ExtensionUniqueId);
try
{
if (installedExtension.IsRunning())
{
installedExtension.SignalDispose();
}
}
catch (Exception ex)
{
Log_FailedSendingDisposeToExtension(ex, installedExtension.ExtensionUniqueId);
}
}
}
public async Task<IEnumerable<IExtensionWrapper>> GetInstalledExtensionsAsync(ProviderType providerType, bool includeDisabledExtensions = false)
{
var installedExtensions = await GetInstalledExtensionsAsync(includeDisabledExtensions);
List<IExtensionWrapper> filteredExtensions = [];
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) => propSet.TryGetValue(name, out var value) ? value as IPropertySet : null;
private static object[]? GetSubPropertySetArray(IPropertySet propSet, string name) => 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 is not 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 is not null)
{
propSetList.Add(classId);
}
}
else
{
var propertySetArray = GetSubPropertySetArray(activationPropSet, CreateInstanceProperty);
if (propertySetArray is not null)
{
foreach (var prop in propertySetArray)
{
if (prop is not IPropertySet propertySet)
{
continue;
}
var classId = GetProperty(propertySet, ClassIdProperty);
if (classId is not null)
{
propSetList.Add(classId);
}
}
}
}
return propSetList;
}
private static string? GetProperty(IPropertySet propSet, string name) => 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;
//} */
[LoggerMessage(
Level = LogLevel.Debug,
Message = "Installed new extension app {extensionName}")]
partial void Log_ExtensionInstalled(string extensionName);
[LoggerMessage(
Level = LogLevel.Debug,
Message = "Uninstalled extension app {extensionPackageName}")]
partial void Log_ExtensionUninstalled(string extensionPackageName);
[LoggerMessage(
Level = LogLevel.Debug,
Message = "Extension {extensionName} declared an unsupported interface: {InterfaceKey}")]
partial void Log_UndeclaredInterfaceInExtension(string extensionName, string interfaceKey);
[LoggerMessage(
Level = LogLevel.Debug,
Message = "Signaling dispose to {extensionUniqueId}")]
partial void Log_DisposingExtension(string extensionUniqueId);
[LoggerMessage(
Level = LogLevel.Error,
Message = "Failed to send dispose signal to extension {extensionUniqueId}")]
partial void Log_FailedSendingDisposeToExtension(Exception ex, string extensionUniqueId);
}
internal record struct IsExtensionResult(bool IsExtension, AppExtension? Extension)
{
}

View File

@@ -4,7 +4,7 @@
using System.Runtime.InteropServices;
using ManagedCommon;
using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CommandPalette.Extensions;
using Windows.ApplicationModel;
using Windows.ApplicationModel.AppExtensions;

View File

@@ -0,0 +1,9 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.UI.ViewModels.Models;
public partial class WinRTExtensionService
{
}

View File

@@ -5,10 +5,9 @@
using System.Diagnostics.CodeAnalysis;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Properties;
using Microsoft.CmdPal.UI.ViewModels.Services;
namespace Microsoft.CmdPal.UI.ViewModels;

View File

@@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Windows.Foundation;
namespace Microsoft.CmdPal.UI.ViewModels.Services;
public interface IExtensionService
{
event TypedEventHandler<CommandProviderWrapper, IEnumerable<CommandProviderWrapper>>? OnCommandProviderAdded;
event TypedEventHandler<CommandProviderWrapper, IEnumerable<CommandProviderWrapper>>? OnCommandProviderRemoved;
event TypedEventHandler<CommandProviderWrapper, IEnumerable<TopLevelViewModel>>? OnCommandsAdded;
event TypedEventHandler<CommandProviderWrapper, IEnumerable<TopLevelViewModel>>? OnCommandsRemoved;
Task<IEnumerable<CommandProviderWrapper>> GetCommandProviderWrappersAsync(WeakReference<IPageContext> weakPageContext, bool includeDisabledExtensions = false);
Task<IEnumerable<TopLevelViewModel>> GetTopLevelCommandsAsync();
Task SignalStopExtensionsAsync();
Task EnableProviderAsync(string providerId);
Task DisableProviderAsync(string providerId);
}

View File

@@ -2,13 +2,10 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.CommandPalette.Extensions;
using Windows.ApplicationModel;
namespace Microsoft.CmdPal.Common.Services;
namespace Microsoft.CmdPal.UI.ViewModels.Services;
public interface IExtensionWrapper
{

View File

@@ -2,17 +2,13 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Diagnostics;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.Common.Helpers;
using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.Extensions.Logging;
namespace Microsoft.CmdPal.UI.ViewModels;
@@ -23,30 +19,30 @@ public partial class TopLevelCommandManager : ObservableObject,
IDisposable
{
private readonly TaskScheduler _taskScheduler;
private readonly IEnumerable<ICommandProvider> _commandProviders;
private readonly IEnumerable<IExtensionService> _extensionServices;
private readonly ILogger _logger;
private readonly SettingsService _settingsService;
private readonly IExtensionService _extensionService;
private readonly List<CommandProviderWrapper> _builtInCommands = [];
private readonly List<CommandProviderWrapper> _extensionCommandProviders = [];
private readonly Lock _commandProvidersLock = new();
private readonly List<CommandProviderWrapper> _commandProviderWrappers = [];
private readonly Lock _commandProviderWrappersLock = new();
private readonly List<TopLevelViewModel> _commandProviderCommands = [];
private readonly Lock _commandProviderCommandsLock = new();
private readonly SupersedingAsyncGate _reloadCommandsGate;
TaskScheduler IPageContext.Scheduler => _taskScheduler;
public TopLevelCommandManager(
IEnumerable<ICommandProvider> commandProviders,
IEnumerable<IExtensionService> extensionServices,
TaskScheduler taskScheduler,
SettingsService settingsService,
IExtensionService extensionService,
ILogger logger)
{
this._logger = logger;
_commandProviders = commandProviders;
_extensionServices = extensionServices;
_taskScheduler = taskScheduler;
_settingsService = settingsService;
_extensionService = extensionService;
WeakReferenceMessenger.Default.Register<ReloadCommandsMessage>(this);
_reloadCommandsGate = new(ReloadAllCommandsAsyncCore);
}
@@ -60,158 +56,80 @@ public partial class TopLevelCommandManager : ObservableObject,
{
get
{
lock (_commandProvidersLock)
lock (_commandProviderWrappersLock)
{
return _builtInCommands.Concat(_extensionCommandProviders).ToList();
return _commandProviderWrappers.ToList();
}
}
}
public async Task<bool> LoadBuiltinsAsync()
[RelayCommand]
public async Task LoadExtensionsAsync()
{
var s = new Stopwatch();
s.Start();
lock (_commandProvidersLock)
lock (_commandProviderWrappersLock)
{
_builtInCommands.Clear();
_commandProviderWrappers.Clear();
}
// Load built-In commands first. These are all in-proc, and
// owned by our ServiceProvider.
var builtInCommands = _commandProviders;
foreach (var provider in builtInCommands)
{
CommandProviderWrapper wrapper = new(provider, _taskScheduler, _hotkeyManager, _aliasManager, _logger);
lock (_commandProvidersLock)
{
_builtInCommands.Add(wrapper);
}
var weakSelf = new WeakReference<IPageContext>(this);
var commands = await LoadTopLevelCommandsFromProvider(wrapper);
lock (TopLevelCommands)
foreach (var extensionService in _extensionServices)
{
// extensionService.OnCommandProviderAdded -= ExtensionService_OnProviderAdded;
// extensionService.OnCommandProviderRemoved -= ExtensionService_OnProviderRemoved;
extensionService.OnCommandsAdded -= ExtensionService_OnCommandsAdded;
extensionService.OnCommandsRemoved -= ExtensionService_OnCommandsRemoved;
_ = Task.Run(async () =>
{
foreach (var c in commands)
var providers = await extensionService.GetCommandProviderWrappersAsync(weakSelf);
lock (_commandProviderWrappersLock)
{
TopLevelCommands.Add(c);
_commandProviderWrappers.AddRange(providers);
}
}
var commands = await extensionService.GetTopLevelCommandsAsync();
lock (_commandProviderCommandsLock)
{
_commandProviderCommands.AddRange(commands);
}
});
// extensionService.OnCommandProviderAdded += ExtensionService_OnProviderAdded;
// extensionService.OnCommandProviderRemoved += ExtensionService_OnProviderRemoved;
extensionService.OnCommandsAdded += ExtensionService_OnCommandsAdded;
extensionService.OnCommandsRemoved += ExtensionService_OnCommandsRemoved;
}
s.Stop();
IsLoading = false;
Log_LoadingBuiltInsTook(s.ElapsedMilliseconds);
return true;
// Send on the current thread; receivers should marshal to UI if needed
WeakReferenceMessenger.Default.Send<ReloadFinishedMessage>();
}
// May be called from a background thread
private async Task<IEnumerable<TopLevelViewModel>> LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider)
private void ExtensionService_OnCommandsAdded(CommandProviderWrapper commandProviderWrapper, IEnumerable<TopLevelViewModel> commands)
{
WeakReference<IPageContext> weakSelf = new(this);
await commandProvider.LoadTopLevelCommands(_settingsService, weakSelf);
var commands = await Task.Factory.StartNew(
() =>
{
List<TopLevelViewModel> commands = [];
foreach (var item in commandProvider.TopLevelItems)
{
commands.Add(item);
}
foreach (var item in commandProvider.FallbackItems)
{
if (item.IsEnabled)
{
commands.Add(item);
}
}
return commands;
},
CancellationToken.None,
TaskCreationOptions.None,
_taskScheduler);
commandProvider.CommandsChanged -= CommandProvider_CommandsChanged;
commandProvider.CommandsChanged += CommandProvider_CommandsChanged;
return commands;
// _ = Task.Run(async () =>
// {
// await Task.Factory.StartNew(
// () =>
// {
// lock (TopLevelCommands)
// {
// foreach (var command in commands)
// {
// TopLevelCommands.Add(command);
// }
// }
// },
// CancellationToken.None,
// TaskCreationOptions.None,
// _taskScheduler);
// });
}
// By all accounts, we're already on a background thread (the COM call
// to handle the event shouldn't be on the main thread.). But just to
// be sure we don't block the caller, hop off this thread
private void CommandProvider_CommandsChanged(CommandProviderWrapper sender, IItemsChangedEventArgs args) =>
_ = Task.Run(async () => await UpdateCommandsForProvider(sender, args));
/// <summary>
/// Called when a command provider raises its ItemsChanged event. We'll
/// remove the old commands from the top-level list and try to put the new
/// ones in the same place in the list.
/// </summary>
/// <param name="sender">The provider who's commands changed</param>
/// <param name="args">the ItemsChangedEvent the provider raised</param>
/// <returns>an awaitable task</returns>
private async Task UpdateCommandsForProvider(CommandProviderWrapper sender, IItemsChangedEventArgs args)
private void ExtensionService_OnCommandsRemoved(CommandProviderWrapper commandProviderWrapper, IEnumerable<TopLevelViewModel> commands)
{
WeakReference<IPageContext> weakSelf = new(this);
await sender.LoadTopLevelCommands(_settingsService, weakSelf);
List<TopLevelViewModel> newItems = [.. sender.TopLevelItems];
foreach (var i in sender.FallbackItems)
{
if (i.IsEnabled)
{
newItems.Add(i);
}
}
// modify the TopLevelCommands under shared lock; event if we clone it, we don't want
// TopLevelCommands to get modified while we're working on it. Otherwise, we might
// out clone would be stale at the end of this method.
lock (TopLevelCommands)
{
// Work on a clone of the list, so that we can just do one atomic
// update to the actual observable list at the end
// TODO: just added a lock around all of this anyway, but keeping the clone
// while looking on some other ways to improve this; can be removed later.
List<TopLevelViewModel> clone = [.. TopLevelCommands];
var startIndex = FindIndexForFirstProviderItem(clone, sender.ProviderId);
clone.RemoveAll(item => item.CommandProviderId == sender.ProviderId);
clone.InsertRange(startIndex, newItems);
ListHelpers.InPlaceUpdateList(TopLevelCommands, clone);
}
return;
static int FindIndexForFirstProviderItem(List<TopLevelViewModel> topLevelItems, string providerId)
{
// Tricky: all Commands from a single provider get added to the
// top-level list all together, in a row. So if we find just the first
// one, we can slice it out and insert the new ones there.
for (var i = 0; i < topLevelItems.Count; i++)
{
var wrapper = topLevelItems[i];
try
{
if (providerId == wrapper.CommandProviderId)
{
return i;
}
}
catch
{
}
}
// If we didn't find any, then we just append the new commands to the end of the list.
return topLevelItems.Count;
}
}
public async Task ReloadAllCommandsAsync()
@@ -225,187 +143,18 @@ public partial class TopLevelCommandManager : ObservableObject,
private async Task ReloadAllCommandsAsyncCore(CancellationToken cancellationToken)
{
IsLoading = true;
await _extensionService.SignalStopExtensionsAsync();
foreach (var extensionService in _extensionServices)
{
await extensionService.SignalStopExtensionsAsync();
}
lock (TopLevelCommands)
{
TopLevelCommands.Clear();
}
await LoadBuiltinsAsync();
_ = Task.Run(LoadExtensionsAsync);
}
// Load commands from our extensions. Called on a background thread.
// Currently, this
// * queries the package catalog,
// * starts all the extensions,
// * then fetches the top-level commands from them.
// TODO In the future, we'll probably abstract some of this away, to have
// separate extension tracking vs stub loading.
[RelayCommand]
public async Task<bool> LoadExtensionsAsync()
{
_extensionService.OnExtensionAdded -= ExtensionService_OnExtensionAdded;
_extensionService.OnExtensionRemoved -= ExtensionService_OnExtensionRemoved;
var extensions = (await _extensionService.GetInstalledExtensionsAsync()).ToImmutableList();
lock (_commandProvidersLock)
{
_extensionCommandProviders.Clear();
}
if (extensions is not null)
{
await StartExtensionsAndGetCommands(extensions);
}
_extensionService.OnExtensionAdded += ExtensionService_OnExtensionAdded;
_extensionService.OnExtensionRemoved += ExtensionService_OnExtensionRemoved;
IsLoading = false;
// Send on the current thread; receivers should marshal to UI if needed
WeakReferenceMessenger.Default.Send<ReloadFinishedMessage>();
return true;
}
private void ExtensionService_OnExtensionAdded(IExtensionService sender, IEnumerable<IExtensionWrapper> extensions)
{
// When we get an extension install event, hop off to a BG thread
_ = Task.Run(async () =>
{
// for each newly installed extension, start it and get commands
// from it. One single package might have more than one
// IExtensionWrapper in it.
await StartExtensionsAndGetCommands(extensions);
});
}
private async Task StartExtensionsAndGetCommands(IEnumerable<IExtensionWrapper> extensions)
{
var timer = new Stopwatch();
timer.Start();
// Start all extensions in parallel
var startTasks = extensions.Select(StartExtensionWithTimeoutAsync);
// Wait for all extensions to start
var wrappers = (await Task.WhenAll(startTasks)).Where(wrapper => wrapper is not null).Select(w => w!).ToList();
lock (_commandProvidersLock)
{
_extensionCommandProviders.AddRange(wrappers);
}
// Load the commands from the providers in parallel
var loadTasks = wrappers.Select(LoadCommandsWithTimeoutAsync);
var commandSets = (await Task.WhenAll(loadTasks)).Where(results => results is not null).Select(r => r!).ToList();
lock (TopLevelCommands)
{
foreach (var commands in commandSets)
{
foreach (var c in commands)
{
TopLevelCommands.Add(c);
}
}
}
timer.Stop();
Log_LoadingExtensionsTook(timer.ElapsedMilliseconds);
}
private async Task<CommandProviderWrapper?> StartExtensionWithTimeoutAsync(IExtensionWrapper extension)
{
Log_StartingExtension(extension.PackageFullName);
try
{
await extension.StartExtensionAsync().WaitAsync(TimeSpan.FromSeconds(10));
return new CommandProviderWrapper(extension, _taskScheduler, _hotkeyManager, _aliasManager, _logger);
}
catch (Exception ex)
{
Log_FailedToStartExtension(extension.PackageFullName, ex);
return null; // Return null for failed extensions
}
}
private async Task<IEnumerable<TopLevelViewModel>?> LoadCommandsWithTimeoutAsync(CommandProviderWrapper wrapper)
{
try
{
return await LoadTopLevelCommandsFromProvider(wrapper!).WaitAsync(TimeSpan.FromSeconds(10));
}
catch (TimeoutException)
{
Log_LoadingCommandsTimedOut(wrapper!.ExtensionHost?.Extension?.PackageFullName);
}
catch (Exception ex)
{
Log_FailedToLoadCommands(wrapper!.ExtensionHost?.Extension?.PackageFullName, ex);
}
return null;
}
private void ExtensionService_OnExtensionRemoved(IExtensionService sender, IEnumerable<IExtensionWrapper> extensions)
{
// When we get an extension uninstall event, hop off to a BG thread
_ = Task.Run(
async () =>
{
// Then find all the top-level commands that belonged to that extension
List<TopLevelViewModel> commandsToRemove = [];
lock (TopLevelCommands)
{
foreach (var extension in extensions)
{
foreach (var command in TopLevelCommands)
{
var host = command.ExtensionHost;
if (host?.Extension == extension)
{
commandsToRemove.Add(command);
}
}
}
}
// Then back on the UI thread (remember, TopLevelCommands is
// Observable, so you can't touch it on the BG thread)...
await Task.Factory.StartNew(
() =>
{
// ... remove all the deleted commands.
lock (TopLevelCommands)
{
if (commandsToRemove.Count != 0)
{
foreach (var deleted in commandsToRemove)
{
TopLevelCommands.Remove(deleted);
}
}
}
},
CancellationToken.None,
TaskCreationOptions.None,
_taskScheduler);
});
_ = Task.Run(LoadExtensionsAsync, cancellationToken);
}
public TopLevelViewModel? LookupCommand(string id)
@@ -435,10 +184,9 @@ public partial class TopLevelCommandManager : ObservableObject,
internal bool IsProviderActive(string id)
{
lock (_commandProvidersLock)
lock (_commandProviderWrappersLock)
{
return _builtInCommands.Any(wrapper => wrapper.Id == id && wrapper.IsActive)
|| _extensionCommandProviders.Any(wrapper => wrapper.Id == id && wrapper.IsActive);
return _commandProviderWrappers.Any(wrapper => wrapper.Id == id && wrapper.IsActive);
}
}

View File

@@ -29,7 +29,6 @@ using Microsoft.CmdPal.UI.Services;
using Microsoft.CmdPal.UI.Settings;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CommandPalette.Extensions;
using Microsoft.Extensions.DependencyInjection;
@@ -130,7 +129,7 @@ public partial class App : Application
private void AddCoreServices(ServiceCollection services)
{
// Core services
services.AddSingleton<IExtensionService, ExtensionService>();
// services.AddSingleton<IExtensionService, WinRTExtensionService>();
services.AddSingleton<IRunHistoryService, RunHistoryService>();
services.AddSingleton<IRootPageService, PowerToysRootPageService>();

View File

@@ -7,7 +7,6 @@ using System.Runtime.InteropServices;
using CmdPalKeyboardService;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.Common.Helpers;
using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.Ext.ClipboardHistory.Messages;
using Microsoft.CmdPal.UI.Controls;
using Microsoft.CmdPal.UI.Events;
@@ -78,7 +77,7 @@ public sealed partial class MainWindow : WindowEx,
private readonly ILogger _logger;
private readonly SettingsService _settingsService;
private readonly TrayIconService _trayIconService;
private readonly IExtensionService _extensionService;
private readonly IEnumerable<IExtensionService> _extensionServices;
private bool _ignoreHotKeyWhenFullScreen = true;
private bool _themeServiceInitialized;
@@ -106,14 +105,14 @@ public sealed partial class MainWindow : WindowEx,
SettingsService settingsService,
TrayIconService trayIconService,
LocalKeyboardListener localKeyboardListener,
IExtensionService extensionService,
IEnumerable<IExtensionService> extensionServices,
ShellPage shellPage,
ILogger logger)
{
InitializeComponent();
_logger = logger;
_trayIconService = trayIconService;
_extensionService = extensionService;
_extensionServices = extensionServices;
ViewModel = mainWindowViewModel;
@@ -751,7 +750,10 @@ public sealed partial class MainWindow : WindowEx,
_settingsService.SaveSettings(settings);
}
_extensionService.SignalStopExtensionsAsync();
foreach (var extensionService in _extensionServices)
{
extensionService.SignalStopExtensionsAsync();
}
_trayIconService.Destroy();

View File

@@ -4,27 +4,34 @@
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using ManagedCommon;
using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.MainPage;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CommandPalette.Extensions;
using Microsoft.Extensions.Logging;
using Windows.Win32.Foundation;
using WinRT;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace Microsoft.CmdPal.UI;
internal sealed class PowerToysRootPageService : IRootPageService
internal sealed partial class PowerToysRootPageService : IRootPageService
{
private readonly TopLevelCommandManager _tlcManager;
private readonly ILogger _logger;
private IExtensionWrapper? _activeExtension;
private Lazy<MainListPage> _mainListPage;
public PowerToysRootPageService(TopLevelCommandManager topLevelCommandManager, SettingsService settingsService, AliasManager aliasManager, AppStateService appStateService)
public PowerToysRootPageService(
TopLevelCommandManager topLevelCommandManager,
SettingsService settingsService,
AliasManager aliasManager,
AppStateService appStateService,
ILogger logger)
{
_logger = logger;
_tlcManager = topLevelCommandManager;
_mainListPage = new Lazy<MainListPage>(() =>
@@ -35,7 +42,7 @@ internal sealed class PowerToysRootPageService : IRootPageService
public async Task PreLoadAsync()
{
await _tlcManager.LoadBuiltinsAsync();
await _tlcManager.LoadExtensionsAsync();
}
public Microsoft.CommandPalette.Extensions.IPage GetRootPage()
@@ -45,14 +52,6 @@ internal sealed class PowerToysRootPageService : IRootPageService
public async Task PostLoadRootPageAsync()
{
// After loading built-ins, and starting navigation, kick off a thread to load extensions.
_tlcManager.LoadExtensionsCommand.Execute(null);
await _tlcManager.LoadExtensionsCommand.ExecutionTask!;
if (_tlcManager.LoadExtensionsCommand.ExecutionTask.Status != TaskStatus.RanToCompletion)
{
// TODO: Handle failure case
}
}
private void OnPerformTopLevelCommand(object? context)
@@ -66,8 +65,7 @@ internal sealed class PowerToysRootPageService : IRootPageService
}
catch (Exception ex)
{
Logger.LogError("Failed to update history in PowerToysRootPageService");
Logger.LogError(ex.ToString());
Log_FailedToUpdateHistory(ex);
}
}
@@ -108,7 +106,7 @@ internal sealed class PowerToysRootPageService : IRootPageService
var hr = Native.CoAllowSetForegroundWindow(intPtr);
if (hr != 0)
{
Logger.LogWarning($"Error giving foreground rights: 0x{hr.Value:X8}");
Log_FailureToGiveForegroundRights(hr);
}
}
}
@@ -138,4 +136,14 @@ internal sealed class PowerToysRootPageService : IRootPageService
[SupportedOSPlatform("windows5.0")]
internal static extern unsafe global::Windows.Win32.Foundation.HRESULT CoAllowSetForegroundWindow(nint pUnk, [Optional] void* lpvReserved);
}
[LoggerMessage(
Level = LogLevel.Error,
Message = "Failed to update history after performing top-level command.")]
partial void Log_FailedToUpdateHistory(Exception ex);
[LoggerMessage(
Level = LogLevel.Error,
Message = "Error giving foreground rights: 0x{hr.Value:X8}")]
partial void Log_FailureToGiveForegroundRights(HRESULT hr);
}