mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-24 04:00:02 +01:00
Working on DI
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user