ExtensionService.cs
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using ManagedCommon; 6 using Microsoft.CmdPal.Core.Common.Services; 7 using Microsoft.CommandPalette.Extensions; 8 using Windows.ApplicationModel; 9 using Windows.ApplicationModel.AppExtensions; 10 using Windows.Foundation; 11 using Windows.Foundation.Collections; 12 13 namespace Microsoft.CmdPal.UI.ViewModels.Models; 14 15 public partial class ExtensionService : IExtensionService, IDisposable 16 { 17 public event TypedEventHandler<IExtensionService, IEnumerable<IExtensionWrapper>>? OnExtensionAdded; 18 19 public event TypedEventHandler<IExtensionService, IEnumerable<IExtensionWrapper>>? OnExtensionRemoved; 20 21 private static readonly PackageCatalog _catalog = PackageCatalog.OpenForCurrentUser(); 22 private static readonly Lock _lock = new(); 23 private readonly SemaphoreSlim _getInstalledExtensionsLock = new(1, 1); 24 private readonly SemaphoreSlim _getInstalledWidgetsLock = new(1, 1); 25 26 // private readonly ILocalSettingsService _localSettingsService; 27 private bool _disposedValue; 28 29 private const string CreateInstanceProperty = "CreateInstance"; 30 private const string ClassIdProperty = "@ClassId"; 31 32 private static readonly List<IExtensionWrapper> _installedExtensions = []; 33 private static readonly List<IExtensionWrapper> _enabledExtensions = []; 34 35 public ExtensionService() 36 { 37 _catalog.PackageInstalling += Catalog_PackageInstalling; 38 _catalog.PackageUninstalling += Catalog_PackageUninstalling; 39 _catalog.PackageUpdating += Catalog_PackageUpdating; 40 41 //// These two were an investigation into getting updates when a package 42 //// gets redeployed from VS. Neither get raised (nor do the above) 43 //// _catalog.PackageStatusChanged += Catalog_PackageStatusChanged; 44 //// _catalog.PackageStaging += Catalog_PackageStaging; 45 // _localSettingsService = settingsService; 46 } 47 48 private void Catalog_PackageInstalling(PackageCatalog sender, PackageInstallingEventArgs args) 49 { 50 if (args.IsComplete) 51 { 52 lock (_lock) 53 { 54 InstallPackageUnderLock(args.Package); 55 } 56 } 57 } 58 59 private void Catalog_PackageUninstalling(PackageCatalog sender, PackageUninstallingEventArgs args) 60 { 61 if (args.IsComplete) 62 { 63 lock (_lock) 64 { 65 UninstallPackageUnderLock(args.Package); 66 } 67 } 68 } 69 70 private void Catalog_PackageUpdating(PackageCatalog sender, PackageUpdatingEventArgs args) 71 { 72 if (args.IsComplete) 73 { 74 lock (_lock) 75 { 76 // Get any extension providers that we previously had from this app 77 UninstallPackageUnderLock(args.TargetPackage); 78 79 // then add the new ones. 80 InstallPackageUnderLock(args.TargetPackage); 81 } 82 } 83 } 84 85 private void InstallPackageUnderLock(Package package) 86 { 87 var isCmdPalExtensionResult = Task.Run(() => 88 { 89 return IsValidCmdPalExtension(package); 90 }).Result; 91 var isExtension = isCmdPalExtensionResult.IsExtension; 92 var extension = isCmdPalExtensionResult.Extension; 93 if (isExtension && extension is not null) 94 { 95 CommandPaletteHost.Instance.DebugLog($"Installed new extension app {extension.DisplayName}"); 96 97 Task.Run(async () => 98 { 99 await _getInstalledExtensionsLock.WaitAsync(); 100 try 101 { 102 var wrappers = await CreateWrappersForExtension(extension); 103 104 UpdateExtensionsListsFromWrappers(wrappers); 105 106 OnExtensionAdded?.Invoke(this, wrappers); 107 } 108 finally 109 { 110 _getInstalledExtensionsLock.Release(); 111 } 112 }); 113 } 114 } 115 116 private void UninstallPackageUnderLock(Package package) 117 { 118 List<IExtensionWrapper> removedExtensions = []; 119 foreach (var extension in _installedExtensions) 120 { 121 if (extension.PackageFullName == package.Id.FullName) 122 { 123 CommandPaletteHost.Instance.DebugLog($"Uninstalled extension app {extension.PackageDisplayName}"); 124 125 removedExtensions.Add(extension); 126 } 127 } 128 129 Task.Run(async () => 130 { 131 await _getInstalledExtensionsLock.WaitAsync(); 132 try 133 { 134 _installedExtensions.RemoveAll(i => removedExtensions.Contains(i)); 135 _enabledExtensions.RemoveAll(i => removedExtensions.Contains(i)); 136 137 OnExtensionRemoved?.Invoke(this, removedExtensions); 138 } 139 finally 140 { 141 _getInstalledExtensionsLock.Release(); 142 } 143 }); 144 } 145 146 private static async Task<IsExtensionResult> IsValidCmdPalExtension(Package package) 147 { 148 var extensions = await AppExtensionCatalog.Open("com.microsoft.commandpalette").FindAllAsync(); 149 foreach (var extension in extensions) 150 { 151 if (package.Id?.FullName == extension.Package?.Id?.FullName) 152 { 153 var (cmdPalProvider, classId) = await GetCmdPalExtensionPropertiesAsync(extension); 154 155 return new(cmdPalProvider is not null && classId.Count != 0, extension); 156 } 157 } 158 159 return new(false, null); 160 } 161 162 private static async Task<(IPropertySet? CmdPalProvider, List<string> ClassIds)> GetCmdPalExtensionPropertiesAsync(AppExtension extension) 163 { 164 var classIds = new List<string>(); 165 var properties = await extension.GetExtensionPropertiesAsync(); 166 167 if (properties is null) 168 { 169 return (null, classIds); 170 } 171 172 var cmdPalProvider = GetSubPropertySet(properties, "CmdPalProvider"); 173 if (cmdPalProvider is null) 174 { 175 return (null, classIds); 176 } 177 178 var activation = GetSubPropertySet(cmdPalProvider, "Activation"); 179 if (activation is null) 180 { 181 return (cmdPalProvider, classIds); 182 } 183 184 // Handle case where extension creates multiple instances. 185 classIds.AddRange(GetCreateInstanceList(activation)); 186 187 return (cmdPalProvider, classIds); 188 } 189 190 private static async Task<IEnumerable<AppExtension>> GetInstalledAppExtensionsAsync() => await AppExtensionCatalog.Open("com.microsoft.commandpalette").FindAllAsync(); 191 192 public async Task<IEnumerable<IExtensionWrapper>> GetInstalledExtensionsAsync(bool includeDisabledExtensions = false) 193 { 194 await _getInstalledExtensionsLock.WaitAsync(); 195 try 196 { 197 if (_installedExtensions.Count == 0) 198 { 199 var extensions = await GetInstalledAppExtensionsAsync(); 200 foreach (var extension in extensions) 201 { 202 var wrappers = await CreateWrappersForExtension(extension); 203 UpdateExtensionsListsFromWrappers(wrappers); 204 } 205 } 206 207 return includeDisabledExtensions ? _installedExtensions : _enabledExtensions; 208 } 209 finally 210 { 211 _getInstalledExtensionsLock.Release(); 212 } 213 } 214 215 private static void UpdateExtensionsListsFromWrappers(List<ExtensionWrapper> wrappers) 216 { 217 foreach (var extensionWrapper in wrappers) 218 { 219 // var localSettingsService = Application.Current.GetService<ILocalSettingsService>(); 220 var extensionUniqueId = extensionWrapper.ExtensionUniqueId; 221 var isExtensionDisabled = false; // await localSettingsService.ReadSettingAsync<bool>(extensionUniqueId + "-ExtensionDisabled"); 222 223 _installedExtensions.Add(extensionWrapper); 224 if (!isExtensionDisabled) 225 { 226 _enabledExtensions.Add(extensionWrapper); 227 } 228 229 // TelemetryFactory.Get<ITelemetry>().Log( 230 // "Extension_ReportInstalled", 231 // LogLevel.Critical, 232 // new ReportInstalledExtensionEvent(extensionUniqueId, isEnabled: !isExtensionDisabled)); 233 } 234 } 235 236 private static async Task<List<ExtensionWrapper>> CreateWrappersForExtension(AppExtension extension) 237 { 238 var (cmdPalProvider, classIds) = await GetCmdPalExtensionPropertiesAsync(extension); 239 240 if (cmdPalProvider is null || classIds.Count == 0) 241 { 242 return []; 243 } 244 245 List<ExtensionWrapper> wrappers = []; 246 foreach (var classId in classIds) 247 { 248 var extensionWrapper = CreateExtensionWrapper(extension, cmdPalProvider, classId); 249 wrappers.Add(extensionWrapper); 250 } 251 252 return wrappers; 253 } 254 255 private static ExtensionWrapper CreateExtensionWrapper(AppExtension extension, IPropertySet cmdPalProvider, string classId) 256 { 257 var extensionWrapper = new ExtensionWrapper(extension, classId); 258 259 var supportedInterfaces = GetSubPropertySet(cmdPalProvider, "SupportedInterfaces"); 260 if (supportedInterfaces is not null) 261 { 262 foreach (var supportedInterface in supportedInterfaces) 263 { 264 ProviderType pt; 265 if (Enum.TryParse(supportedInterface.Key, out pt)) 266 { 267 extensionWrapper.AddProviderType(pt); 268 } 269 else 270 { 271 // log warning that extension declared unsupported extension interface 272 CommandPaletteHost.Instance.DebugLog($"Extension {extension.DisplayName} declared an unsupported interface: {supportedInterface.Key}"); 273 } 274 } 275 } 276 277 return extensionWrapper; 278 } 279 280 public IExtensionWrapper? GetInstalledExtension(string extensionUniqueId) 281 { 282 var extension = _installedExtensions.Where(extension => extension.ExtensionUniqueId.Equals(extensionUniqueId, StringComparison.Ordinal)); 283 return extension.FirstOrDefault(); 284 } 285 286 public async Task SignalStopExtensionsAsync() 287 { 288 var installedExtensions = await GetInstalledExtensionsAsync(); 289 foreach (var installedExtension in installedExtensions) 290 { 291 Logger.LogDebug($"Signaling dispose to {installedExtension.ExtensionUniqueId}"); 292 try 293 { 294 if (installedExtension.IsRunning()) 295 { 296 installedExtension.SignalDispose(); 297 } 298 } 299 catch (Exception ex) 300 { 301 Logger.LogError($"Failed to send dispose signal to extension {installedExtension.ExtensionUniqueId}", ex); 302 } 303 } 304 } 305 306 public async Task<IEnumerable<IExtensionWrapper>> GetInstalledExtensionsAsync(ProviderType providerType, bool includeDisabledExtensions = false) 307 { 308 var installedExtensions = await GetInstalledExtensionsAsync(includeDisabledExtensions); 309 310 List<IExtensionWrapper> filteredExtensions = []; 311 foreach (var installedExtension in installedExtensions) 312 { 313 if (installedExtension.HasProviderType(providerType)) 314 { 315 filteredExtensions.Add(installedExtension); 316 } 317 } 318 319 return filteredExtensions; 320 } 321 322 public void Dispose() 323 { 324 Dispose(disposing: true); 325 GC.SuppressFinalize(this); 326 } 327 328 protected virtual void Dispose(bool disposing) 329 { 330 if (!_disposedValue) 331 { 332 if (disposing) 333 { 334 _getInstalledExtensionsLock.Dispose(); 335 _getInstalledWidgetsLock.Dispose(); 336 } 337 338 _disposedValue = true; 339 } 340 } 341 342 private static IPropertySet? GetSubPropertySet(IPropertySet propSet, string name) => propSet.TryGetValue(name, out var value) ? value as IPropertySet : null; 343 344 private static object[]? GetSubPropertySetArray(IPropertySet propSet, string name) => propSet.TryGetValue(name, out var value) ? value as object[] : null; 345 346 /// <summary> 347 /// There are cases where the extension creates multiple COM instances. 348 /// </summary> 349 /// <param name="activationPropSet">Activation property set object</param> 350 /// <returns>List of ClassId strings associated with the activation property</returns> 351 private static List<string> GetCreateInstanceList(IPropertySet activationPropSet) 352 { 353 var propSetList = new List<string>(); 354 var singlePropertySet = GetSubPropertySet(activationPropSet, CreateInstanceProperty); 355 if (singlePropertySet is not null) 356 { 357 var classId = GetProperty(singlePropertySet, ClassIdProperty); 358 359 // If the instance has a classId as a single string, then it's only supporting a single instance. 360 if (classId is not null) 361 { 362 propSetList.Add(classId); 363 } 364 } 365 else 366 { 367 var propertySetArray = GetSubPropertySetArray(activationPropSet, CreateInstanceProperty); 368 if (propertySetArray is not null) 369 { 370 foreach (var prop in propertySetArray) 371 { 372 if (prop is not IPropertySet propertySet) 373 { 374 continue; 375 } 376 377 var classId = GetProperty(propertySet, ClassIdProperty); 378 if (classId is not null) 379 { 380 propSetList.Add(classId); 381 } 382 } 383 } 384 } 385 386 return propSetList; 387 } 388 389 private static string? GetProperty(IPropertySet propSet, string name) => propSet[name] as string; 390 391 public void EnableExtension(string extensionUniqueId) 392 { 393 var extension = _installedExtensions.Where(extension => extension.ExtensionUniqueId.Equals(extensionUniqueId, StringComparison.Ordinal)); 394 _enabledExtensions.Add(extension.First()); 395 } 396 397 public void DisableExtension(string extensionUniqueId) 398 { 399 var extension = _enabledExtensions.Where(extension => extension.ExtensionUniqueId.Equals(extensionUniqueId, StringComparison.Ordinal)); 400 _enabledExtensions.Remove(extension.First()); 401 } 402 403 /* 404 ///// <inheritdoc cref="IExtensionService.DisableExtensionIfWindowsFeatureNotAvailable(IExtensionWrapper)"/> 405 //public async Task<bool> DisableExtensionIfWindowsFeatureNotAvailable(IExtensionWrapper extension) 406 //{ 407 // // Only attempt to disable feature if its available. 408 // if (IsWindowsOptionalFeatureAvailableForExtension(extension.ExtensionClassId)) 409 // { 410 // return false; 411 // } 412 // _log.Warning($"Disabling extension: '{extension.ExtensionDisplayName}' because its feature is absent or unknown"); 413 // // Remove extension from list of enabled extensions to prevent Dev Home from re-querying for this extension 414 // // for the rest of its process lifetime. 415 // DisableExtension(extension.ExtensionUniqueId); 416 // // Update the local settings so the next time the user launches Dev Home the extension will be disabled. 417 // await _localSettingsService.SaveSettingAsync(extension.ExtensionUniqueId + "-ExtensionDisabled", true); 418 // return true; 419 //} */ 420 } 421 422 internal record struct IsExtensionResult(bool IsExtension, AppExtension? Extension) 423 { 424 }