From ba4bb7b2080d74918d6d856fba6f86caa410149b Mon Sep 17 00:00:00 2001 From: Rob Mensching <rob@firegiant.com> Date: Sat, 19 Mar 2022 22:53:06 -0700 Subject: Centralize cache locations in IExtensionManager This removes the duplication of cache location definitions between IExtensionManager and extension cache command. Also, adds an extension cache test. Fixes 6536 --- .../Data/IExtensionCacheLocation.cs | 41 ++++++++++++ .../Services/IExtensionManager.cs | 7 ++ .../ExtensionCacheManager.cs | 66 ++++++++++++++----- .../ExtensionCacheManagerCommand.cs | 9 ++- .../ExtensionCacheWarnings.cs | 24 +++++++ .../WixToolsetCoreServiceProviderExtensions.cs | 5 +- src/wix/WixToolset.Core.TestPackage/WixRunner.cs | 4 +- .../WixToolset.Core.TestPackage.csproj | 1 + .../ExtensionCacheLocation.cs | 19 ++++++ .../ExtensibilityServices/ExtensionManager.cs | 74 ++++++++++------------ .../ExtensionFixture.cs | 46 ++++++++++++++ 11 files changed, 235 insertions(+), 61 deletions(-) create mode 100644 src/api/wix/WixToolset.Extensibility/Data/IExtensionCacheLocation.cs create mode 100644 src/wix/WixToolset.Core.ExtensionCache/ExtensionCacheWarnings.cs create mode 100644 src/wix/WixToolset.Core/ExtensibilityServices/ExtensionCacheLocation.cs diff --git a/src/api/wix/WixToolset.Extensibility/Data/IExtensionCacheLocation.cs b/src/api/wix/WixToolset.Extensibility/Data/IExtensionCacheLocation.cs new file mode 100644 index 00000000..53158875 --- /dev/null +++ b/src/api/wix/WixToolset.Extensibility/Data/IExtensionCacheLocation.cs @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.Extensibility.Data +{ + /// <summary> + /// Extension cache location scope. + /// </summary> + public enum ExtensionCacheLocationScope + { + /// <summary> + /// Project extension cache location. + /// </summary> + Project, + + /// <summary> + /// User extension cache location. + /// </summary> + User, + + /// <summary> + /// Machine extension cache location. + /// </summary> + Machine, + } + + /// <summary> + /// Location where extensions may be cached. + /// </summary> + public interface IExtensionCacheLocation + { + /// <summary> + /// Path for the extension cache location. + /// </summary> + string Path { get; } + + /// <summary> + /// Scope for the extension cache location. + /// </summary> + ExtensionCacheLocationScope Scope { get; } + } +} diff --git a/src/api/wix/WixToolset.Extensibility/Services/IExtensionManager.cs b/src/api/wix/WixToolset.Extensibility/Services/IExtensionManager.cs index 8e49c38d..fe939a59 100644 --- a/src/api/wix/WixToolset.Extensibility/Services/IExtensionManager.cs +++ b/src/api/wix/WixToolset.Extensibility/Services/IExtensionManager.cs @@ -4,6 +4,7 @@ namespace WixToolset.Extensibility.Services { using System.Collections.Generic; using System.Reflection; + using WixToolset.Extensibility.Data; /// <summary> /// Loads extensions and uses the extensions' factories to provide services. @@ -32,6 +33,12 @@ namespace WixToolset.Extensibility.Services /// </remarks> void Load(string extensionReference); + /// <summary> + /// Gets extensions cache locations. + /// </summary> + /// <returns>List of cache locations where extensions may be found.</returns> + IReadOnlyCollection<IExtensionCacheLocation> GetCacheLocations(); + /// <summary> /// Gets extensions of specified type from factories loaded into the extension manager. /// </summary> diff --git a/src/wix/WixToolset.Core.ExtensionCache/ExtensionCacheManager.cs b/src/wix/WixToolset.Core.ExtensionCache/ExtensionCacheManager.cs index 256eeb0b..b50ad6e8 100644 --- a/src/wix/WixToolset.Core.ExtensionCache/ExtensionCacheManager.cs +++ b/src/wix/WixToolset.Core.ExtensionCache/ExtensionCacheManager.cs @@ -15,22 +15,26 @@ namespace WixToolset.Core.ExtensionCache using NuGet.Protocol; using NuGet.Protocol.Core.Types; using NuGet.Versioning; + using WixToolset.Extensibility.Data; + using WixToolset.Extensibility.Services; /// <summary> /// Extension cache manager. /// </summary> internal class ExtensionCacheManager { - public string CacheFolder(bool global) => global ? this.GlobalCacheFolder() : this.LocalCacheFolder(); + private IReadOnlyCollection<IExtensionCacheLocation> cacheLocations; - public string LocalCacheFolder() => Path.Combine(Environment.CurrentDirectory, ".wix", "extensions"); - - public string GlobalCacheFolder() + public ExtensionCacheManager(IMessaging messaging, IExtensionManager extensionManager) { - var baseFolder = Environment.GetEnvironmentVariable("WIX_EXTENSIONS") ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - return Path.Combine(baseFolder, ".wix", "extensions"); + this.Messaging = messaging; + this.ExtensionManager = extensionManager; } + private IMessaging Messaging { get; } + + private IExtensionManager ExtensionManager { get; } + public async Task<bool> AddAsync(bool global, string extension, CancellationToken cancellationToken) { if (String.IsNullOrEmpty(extension)) @@ -54,11 +58,11 @@ namespace WixToolset.Core.ExtensionCache (var extensionId, var extensionVersion) = ParseExtensionReference(extension); - var cacheFolder = this.CacheFolder(global); + var cacheFolder = this.GetCacheFolder(global); - cacheFolder = Path.Combine(cacheFolder, extensionId, extensionVersion); + var extensionFolder = Path.Combine(cacheFolder, extensionId, extensionVersion); - if (Directory.Exists(cacheFolder)) + if (Directory.Exists(extensionFolder)) { cancellationToken.ThrowIfCancellationRequested(); @@ -75,7 +79,7 @@ namespace WixToolset.Core.ExtensionCache (var extensionId, var extensionVersion) = ParseExtensionReference(extension); - var cacheFolder = this.CacheFolder(global); + var cacheFolder = this.GetCacheFolder(global); var searchFolder = Path.Combine(cacheFolder, extensionId, extensionVersion); @@ -127,6 +131,20 @@ namespace WixToolset.Core.ExtensionCache return Task.FromResult((IEnumerable<CachedExtension>)found); } + private string GetCacheFolder(bool global) + { + if (this.cacheLocations == null) + { + this.cacheLocations = this.ExtensionManager.GetCacheLocations(); + } + + var requestedScope = global ? ExtensionCacheLocationScope.User : ExtensionCacheLocationScope.Project; + + var cacheLocation = this.cacheLocations.First(l => l.Scope == requestedScope); + + return cacheLocation.Path; + } + private async Task<bool> DownloadAndExtractAsync(bool global, string id, string version, CancellationToken cancellationToken) { var logger = NullLogger.Instance; @@ -149,15 +167,22 @@ namespace WixToolset.Core.ExtensionCache var repository = Repository.Factory.GetCoreV3(source.Source); var resource = await repository.GetResourceAsync<FindPackageByIdResource>(); - var availableVersions = await resource.GetAllVersionsAsync(id, cache, logger, cancellationToken); - foreach (var availableVersion in availableVersions) + try { - if (nugetVersion is null || nugetVersion < availableVersion) + var availableVersions = await resource.GetAllVersionsAsync(id, cache, logger, cancellationToken); + foreach (var availableVersion in availableVersions) { - nugetVersion = availableVersion; - versionSource = source; + if (nugetVersion is null || nugetVersion < availableVersion) + { + nugetVersion = availableVersion; + versionSource = source; + } } } + catch (FatalProtocolException e) + { + this.Messaging.Write(ExtensionCacheWarnings.NugetException(id, e.Message)); + } } if (nugetVersion is null) @@ -168,7 +193,9 @@ namespace WixToolset.Core.ExtensionCache var searchSources = versionSource is null ? sources : new[] { versionSource }; - var extensionFolder = Path.Combine(this.CacheFolder(global), id, nugetVersion.ToString()); + var cacheFolder = this.GetCacheFolder(global); + + var extensionFolder = Path.Combine(cacheFolder, id, nugetVersion.ToString()); foreach (var source in searchSources) { @@ -183,6 +210,8 @@ namespace WixToolset.Core.ExtensionCache { stream.Position = 0; + Directory.CreateDirectory(extensionFolder); + using (var archive = new PackageArchiveReader(stream)) { var files = PackagingConstants.Folders.Known.SelectMany(folder => archive.GetFiles(folder)).Distinct(StringComparer.OrdinalIgnoreCase); @@ -198,7 +227,10 @@ namespace WixToolset.Core.ExtensionCache return false; } - private string ExtractProgress(string sourceFile, string targetPath, Stream fileStream) => fileStream.CopyToFile(targetPath); + private string ExtractProgress(string sourceFile, string targetPath, Stream fileStream) + { + return fileStream.CopyToFile(targetPath); + } private static (string extensionId, string extensionVersion) ParseExtensionReference(string extensionReference) { diff --git a/src/wix/WixToolset.Core.ExtensionCache/ExtensionCacheManagerCommand.cs b/src/wix/WixToolset.Core.ExtensionCache/ExtensionCacheManagerCommand.cs index d37ee341..008fcebd 100644 --- a/src/wix/WixToolset.Core.ExtensionCache/ExtensionCacheManagerCommand.cs +++ b/src/wix/WixToolset.Core.ExtensionCache/ExtensionCacheManagerCommand.cs @@ -25,17 +25,20 @@ namespace WixToolset.Core.ExtensionCache public ExtensionCacheManagerCommand(IServiceProvider serviceProvider) { this.Messaging = serviceProvider.GetService<IMessaging>(); + this.ExtensionManager = serviceProvider.GetService<IExtensionManager>(); this.ExtensionReferences = new List<string>(); } - private IMessaging Messaging { get; } - public bool ShowHelp { get; set; } public bool ShowLogo { get; set; } public bool StopParsing { get; set; } + private IMessaging Messaging { get; } + + private IExtensionManager ExtensionManager { get; } + private bool Global { get; set; } private CacheSubcommand? Subcommand { get; set; } @@ -51,7 +54,7 @@ namespace WixToolset.Core.ExtensionCache } var success = false; - var cacheManager = new ExtensionCacheManager(); + var cacheManager = new ExtensionCacheManager(this.Messaging, this.ExtensionManager); switch (this.Subcommand) { diff --git a/src/wix/WixToolset.Core.ExtensionCache/ExtensionCacheWarnings.cs b/src/wix/WixToolset.Core.ExtensionCache/ExtensionCacheWarnings.cs new file mode 100644 index 00000000..ddcbfdea --- /dev/null +++ b/src/wix/WixToolset.Core.ExtensionCache/ExtensionCacheWarnings.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.Core.ExtensionCache +{ + using WixToolset.Data; + + internal static class ExtensionCacheWarnings + { + public static Message NugetException(string extensionId, string exceptionMessage) + { + return Message(new SourceLineNumber(extensionId), Ids.NugetException, "{0}", exceptionMessage); + } + + private static Message Message(SourceLineNumber sourceLineNumber, Ids id, string format, params object[] args) + { + return new Message(sourceLineNumber, MessageLevel.Warning, (int)id, format, args); + } + + public enum Ids + { + NugetException = 6100, + } // last available is 6499. 6500 is ExtensionCacheErrors. + } +} diff --git a/src/wix/WixToolset.Core.ExtensionCache/WixToolsetCoreServiceProviderExtensions.cs b/src/wix/WixToolset.Core.ExtensionCache/WixToolsetCoreServiceProviderExtensions.cs index 424fc469..b591661e 100644 --- a/src/wix/WixToolset.Core.ExtensionCache/WixToolsetCoreServiceProviderExtensions.cs +++ b/src/wix/WixToolset.Core.ExtensionCache/WixToolsetCoreServiceProviderExtensions.cs @@ -27,7 +27,10 @@ namespace WixToolset.Core.ExtensionCache private static ExtensionCacheManager CreateExtensionCacheManager(IWixToolsetCoreServiceProvider coreProvider, Dictionary<Type, object> singletons) { - var extensionCacheManager = new ExtensionCacheManager(); + var messaging = coreProvider.GetService<IMessaging>(); + var extensionManager = coreProvider.GetService<IExtensionManager>(); + + var extensionCacheManager = new ExtensionCacheManager(messaging, extensionManager); singletons.Add(typeof(ExtensionCacheManager), extensionCacheManager); return extensionCacheManager; diff --git a/src/wix/WixToolset.Core.TestPackage/WixRunner.cs b/src/wix/WixToolset.Core.TestPackage/WixRunner.cs index ed7c49b8..8ad3af9a 100644 --- a/src/wix/WixToolset.Core.TestPackage/WixRunner.cs +++ b/src/wix/WixToolset.Core.TestPackage/WixRunner.cs @@ -7,6 +7,7 @@ namespace WixToolset.Core.TestPackage using System.Threading; using System.Threading.Tasks; using WixToolset.Core.Burn; + using WixToolset.Core.ExtensionCache; using WixToolset.Core.WindowsInstaller; using WixToolset.Data; using WixToolset.Extensibility.Services; @@ -65,7 +66,8 @@ namespace WixToolset.Core.TestPackage public static Task<int> Execute(string[] args, IWixToolsetCoreServiceProvider coreProvider, out List<Message> messages, bool warningsAsErrors = true) { coreProvider.AddWindowsInstallerBackend() - .AddBundleBackend(); + .AddBundleBackend() + .AddExtensionCacheManager(); var listener = new TestMessageListener(); diff --git a/src/wix/WixToolset.Core.TestPackage/WixToolset.Core.TestPackage.csproj b/src/wix/WixToolset.Core.TestPackage/WixToolset.Core.TestPackage.csproj index bda54cd1..0c78cc32 100644 --- a/src/wix/WixToolset.Core.TestPackage/WixToolset.Core.TestPackage.csproj +++ b/src/wix/WixToolset.Core.TestPackage/WixToolset.Core.TestPackage.csproj @@ -16,6 +16,7 @@ <ProjectReference Include="..\WixToolset.Core.Native\WixToolset.Core.Native.csproj" PrivateAssets="true" /> <ProjectReference Include="..\WixToolset.Core\WixToolset.Core.csproj" PrivateAssets="true" /> <ProjectReference Include="..\WixToolset.Core.Burn\WixToolset.Core.Burn.csproj" PrivateAssets="true" /> + <ProjectReference Include="..\WixToolset.Core.ExtensionCache\WixToolset.Core.ExtensionCache.csproj" PrivateAssets="true" /> <ProjectReference Include="..\WixToolset.Core.WindowsInstaller\WixToolset.Core.WindowsInstaller.csproj" PrivateAssets="true" /> </ItemGroup> diff --git a/src/wix/WixToolset.Core/ExtensibilityServices/ExtensionCacheLocation.cs b/src/wix/WixToolset.Core/ExtensibilityServices/ExtensionCacheLocation.cs new file mode 100644 index 00000000..b45cd315 --- /dev/null +++ b/src/wix/WixToolset.Core/ExtensibilityServices/ExtensionCacheLocation.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.Core.ExtensibilityServices +{ + using WixToolset.Extensibility.Data; + + internal class ExtensionCacheLocation : IExtensionCacheLocation + { + public ExtensionCacheLocation(string path, ExtensionCacheLocationScope scope) + { + this.Path = path; + this.Scope = scope; + } + + public string Path { get; } + + public ExtensionCacheLocationScope Scope { get; } + } +} diff --git a/src/wix/WixToolset.Core/ExtensibilityServices/ExtensionManager.cs b/src/wix/WixToolset.Core/ExtensibilityServices/ExtensionManager.cs index 2340ed9e..71334841 100644 --- a/src/wix/WixToolset.Core/ExtensibilityServices/ExtensionManager.cs +++ b/src/wix/WixToolset.Core/ExtensibilityServices/ExtensionManager.cs @@ -9,6 +9,7 @@ namespace WixToolset.Core.ExtensibilityServices using System.Reflection; using WixToolset.Data; using WixToolset.Extensibility; + using WixToolset.Extensibility.Data; using WixToolset.Extensibility.Services; internal class ExtensionManager : IExtensionManager @@ -16,6 +17,7 @@ namespace WixToolset.Core.ExtensibilityServices private const string UserWixFolderName = ".wix4"; private const string MachineWixFolderName = "WixToolset4"; private const string ExtensionsFolderName = "extensions"; + private const string UserEnvironmentName = "WIX_EXTENSIONS"; private readonly List<IExtensionFactory> extensionFactories = new List<IExtensionFactory>(); private readonly Dictionary<Type, List<object>> loadedExtensionsByType = new Dictionary<Type, List<object>>(); @@ -51,9 +53,14 @@ namespace WixToolset.Core.ExtensibilityServices { if (TryParseExtensionReference(extensionPath, out var extensionId, out var extensionVersion)) { - foreach (var cachePath in this.CacheLocations()) + foreach (var cacheLocation in this.GetCacheLocations()) { - var extensionFolder = Path.Combine(cachePath, extensionId); + var extensionFolder = Path.Combine(cacheLocation.Path, extensionId); + + if (!Directory.Exists(extensionFolder)) + { + continue; + } var versionFolder = extensionVersion; if (String.IsNullOrEmpty(versionFolder) && !TryFindLatestVersionInFolder(extensionFolder, out versionFolder)) @@ -94,6 +101,32 @@ namespace WixToolset.Core.ExtensibilityServices } } + public IReadOnlyCollection<IExtensionCacheLocation> GetCacheLocations() + { + var locations = new List<IExtensionCacheLocation>(); + + var path = Path.Combine(Environment.CurrentDirectory, UserWixFolderName, ExtensionsFolderName); + locations.Add(new ExtensionCacheLocation(path, ExtensionCacheLocationScope.Project)); + + path = Environment.GetEnvironmentVariable(UserEnvironmentName) ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + path = Path.Combine(path, UserWixFolderName, ExtensionsFolderName); + locations.Add(new ExtensionCacheLocation(path, ExtensionCacheLocationScope.User)); + + if (Environment.Is64BitOperatingSystem) + { + path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonProgramFiles), MachineWixFolderName, ExtensionsFolderName); + locations.Add(new ExtensionCacheLocation(path, ExtensionCacheLocationScope.Machine)); + } + + path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonProgramFilesX86), MachineWixFolderName, ExtensionsFolderName); + locations.Add(new ExtensionCacheLocation(path, ExtensionCacheLocationScope.Machine)); + + path = Path.Combine(Path.GetDirectoryName(new Uri(Assembly.GetCallingAssembly().CodeBase).LocalPath), ExtensionsFolderName); + locations.Add(new ExtensionCacheLocation(path, ExtensionCacheLocationScope.Machine)); + + return locations; + } + public IReadOnlyCollection<T> GetServices<T>() where T : class { if (!this.loadedExtensionsByType.TryGetValue(typeof(T), out var extensions)) @@ -125,43 +158,6 @@ namespace WixToolset.Core.ExtensibilityServices return (IExtensionFactory)Activator.CreateInstance(type); } - private IEnumerable<string> CacheLocations() - { - var path = Path.Combine(Environment.CurrentDirectory, UserWixFolderName, ExtensionsFolderName); - if (Directory.Exists(path)) - { - yield return path; - } - - path = Environment.GetEnvironmentVariable("WIX_EXTENSIONS") ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - path = Path.Combine(path, UserWixFolderName, ExtensionsFolderName); - if (Directory.Exists(path)) - { - yield return path; - } - - if (Environment.Is64BitOperatingSystem) - { - path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonProgramFiles), MachineWixFolderName, ExtensionsFolderName); - if (Directory.Exists(path)) - { - yield return path; - } - } - - path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonProgramFilesX86), MachineWixFolderName, ExtensionsFolderName); - if (Directory.Exists(path)) - { - yield return path; - } - - path = Path.Combine(Path.GetDirectoryName(new Uri(Assembly.GetCallingAssembly().CodeBase).LocalPath), ExtensionsFolderName); - if (Directory.Exists(path)) - { - yield return path; - } - } - private static bool TryParseExtensionReference(string extensionReference, out string extensionId, out string extensionVersion) { extensionId = extensionReference ?? String.Empty; diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/ExtensionFixture.cs b/src/wix/test/WixToolsetTest.CoreIntegration/ExtensionFixture.cs index 225355bf..d814000c 100644 --- a/src/wix/test/WixToolsetTest.CoreIntegration/ExtensionFixture.cs +++ b/src/wix/test/WixToolsetTest.CoreIntegration/ExtensionFixture.cs @@ -144,6 +144,52 @@ namespace WixToolsetTest.CoreIntegration } } + [Fact] + public void CanManipulateExtensionCache() + { + var currentFolder = Environment.CurrentDirectory; + + try + { + using (var fs = new DisposableFileSystem()) + { + var folder = fs.GetFolder(true); + Environment.CurrentDirectory = folder; + + var result = WixRunner.Execute(new[] + { + "extension", "add", "WixToolset.UI.wixext" + }); + + result.AssertSuccess(); + + var cacheFolder = Path.Combine(folder, ".wix4", "extensions", "WixToolset.UI.wixext"); + Assert.True(Directory.Exists(cacheFolder), $"Expected folder '{cacheFolder}' to exist"); + + result = WixRunner.Execute(new[] + { + "extension", "list" + }); + + result.AssertSuccess(); + var output = result.Messages.Select(m => m.ToString()).Single(); + Assert.StartsWith("WixToolset.UI.wixext 4.", output); + + result = WixRunner.Execute(new[] + { + "extension", "remove", "WixToolset.UI.wixext" + }); + + result.AssertSuccess(); + Assert.False(Directory.Exists(cacheFolder), $"Expected folder '{cacheFolder}' to NOT exist"); + } + } + finally + { + Environment.CurrentDirectory = currentFolder; + } + } + private static void Build(string[] args) { var result = WixRunner.Execute(args) -- cgit v1.2.3-55-g6feb