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  }