@joebigelow / wix-1 / commits / ba4bb7b2

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

Rob Mensching committed Mar 19, 2022 at 22:53 UTC ba4bb7b2080d74918d6d856fba6f86caa410149b
11 files changed +235 -61
src/api/wix/WixToolset.Extensibility/Data/IExtensionCacheLocation.cs new
+41
@@ -0,0 +1,41 @@
1 +// 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.
2 +
3 +namespace WixToolset.Extensibility.Data
4 +{
5 + /// <summary>
6 + /// Extension cache location scope.
7 + /// </summary>
8 + public enum ExtensionCacheLocationScope
9 + {
10 + /// <summary>
11 + /// Project extension cache location.
12 + /// </summary>
13 + Project,
14 +
15 + /// <summary>
16 + /// User extension cache location.
17 + /// </summary>
18 + User,
19 +
20 + /// <summary>
21 + /// Machine extension cache location.
22 + /// </summary>
23 + Machine,
24 + }
25 +
26 + /// <summary>
27 + /// Location where extensions may be cached.
28 + /// </summary>
29 + public interface IExtensionCacheLocation
30 + {
31 + /// <summary>
32 + /// Path for the extension cache location.
33 + /// </summary>
34 + string Path { get; }
35 +
36 + /// <summary>
37 + /// Scope for the extension cache location.
38 + /// </summary>
39 + ExtensionCacheLocationScope Scope { get; }
40 + }
41 +}
src/api/wix/WixToolset.Extensibility/Services/IExtensionManager.cs
+7
@@ -4,6 +4,7 @@ namespace WixToolset.Extensibility.Services
4 {
5 using System.Collections.Generic;
6 using System.Reflection;
7 + using WixToolset.Extensibility.Data;
8
9 /// <summary>
10 /// Loads extensions and uses the extensions' factories to provide services.
@@ -32,6 +33,12 @@ namespace WixToolset.Extensibility.Services
33 /// </remarks>
34 void Load(string extensionReference);
35
36 + /// <summary>
37 + /// Gets extensions cache locations.
38 + /// </summary>
39 + /// <returns>List of cache locations where extensions may be found.</returns>
40 + IReadOnlyCollection<IExtensionCacheLocation> GetCacheLocations();
41 +
42 /// <summary>
43 /// Gets extensions of specified type from factories loaded into the extension manager.
44 /// </summary>
src/wix/WixToolset.Core.ExtensionCache/ExtensionCacheManager.cs
+49 -17
@@ -15,22 +15,26 @@ namespace WixToolset.Core.ExtensionCache
15 using NuGet.Protocol;
16 using NuGet.Protocol.Core.Types;
17 using NuGet.Versioning;
18 + using WixToolset.Extensibility.Data;
19 + using WixToolset.Extensibility.Services;
20
21 /// <summary>
22 /// Extension cache manager.
23 /// </summary>
24 internal class ExtensionCacheManager
25 {
24 - public string CacheFolder(bool global) => global ? this.GlobalCacheFolder() : this.LocalCacheFolder();
26 + private IReadOnlyCollection<IExtensionCacheLocation> cacheLocations;
27
26 - public string LocalCacheFolder() => Path.Combine(Environment.CurrentDirectory, ".wix", "extensions");
27 -
28 - public string GlobalCacheFolder()
28 + public ExtensionCacheManager(IMessaging messaging, IExtensionManager extensionManager)
29 {
30 - var baseFolder = Environment.GetEnvironmentVariable("WIX_EXTENSIONS") ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
31 - return Path.Combine(baseFolder, ".wix", "extensions");
30 + this.Messaging = messaging;
31 + this.ExtensionManager = extensionManager;
32 }
33
34 + private IMessaging Messaging { get; }
35 +
36 + private IExtensionManager ExtensionManager { get; }
37 +
38 public async Task<bool> AddAsync(bool global, string extension, CancellationToken cancellationToken)
39 {
40 if (String.IsNullOrEmpty(extension))
@@ -54,11 +58,11 @@ namespace WixToolset.Core.ExtensionCache
58
59 (var extensionId, var extensionVersion) = ParseExtensionReference(extension);
60
57 - var cacheFolder = this.CacheFolder(global);
61 + var cacheFolder = this.GetCacheFolder(global);
62
59 - cacheFolder = Path.Combine(cacheFolder, extensionId, extensionVersion);
63 + var extensionFolder = Path.Combine(cacheFolder, extensionId, extensionVersion);
64
61 - if (Directory.Exists(cacheFolder))
65 + if (Directory.Exists(extensionFolder))
66 {
67 cancellationToken.ThrowIfCancellationRequested();
68
@@ -75,7 +79,7 @@ namespace WixToolset.Core.ExtensionCache
79
80 (var extensionId, var extensionVersion) = ParseExtensionReference(extension);
81
78 - var cacheFolder = this.CacheFolder(global);
82 + var cacheFolder = this.GetCacheFolder(global);
83
84 var searchFolder = Path.Combine(cacheFolder, extensionId, extensionVersion);
85
@@ -127,6 +131,20 @@ namespace WixToolset.Core.ExtensionCache
131 return Task.FromResult((IEnumerable<CachedExtension>)found);
132 }
133
134 + private string GetCacheFolder(bool global)
135 + {
136 + if (this.cacheLocations == null)
137 + {
138 + this.cacheLocations = this.ExtensionManager.GetCacheLocations();
139 + }
140 +
141 + var requestedScope = global ? ExtensionCacheLocationScope.User : ExtensionCacheLocationScope.Project;
142 +
143 + var cacheLocation = this.cacheLocations.First(l => l.Scope == requestedScope);
144 +
145 + return cacheLocation.Path;
146 + }
147 +
148 private async Task<bool> DownloadAndExtractAsync(bool global, string id, string version, CancellationToken cancellationToken)
149 {
150 var logger = NullLogger.Instance;
@@ -149,15 +167,22 @@ namespace WixToolset.Core.ExtensionCache
167 var repository = Repository.Factory.GetCoreV3(source.Source);
168 var resource = await repository.GetResourceAsync<FindPackageByIdResource>();
169
152 - var availableVersions = await resource.GetAllVersionsAsync(id, cache, logger, cancellationToken);
153 - foreach (var availableVersion in availableVersions)
170 + try
171 {
155 - if (nugetVersion is null || nugetVersion < availableVersion)
172 + var availableVersions = await resource.GetAllVersionsAsync(id, cache, logger, cancellationToken);
173 + foreach (var availableVersion in availableVersions)
174 {
157 - nugetVersion = availableVersion;
158 - versionSource = source;
175 + if (nugetVersion is null || nugetVersion < availableVersion)
176 + {
177 + nugetVersion = availableVersion;
178 + versionSource = source;
179 + }
180 }
181 }
182 + catch (FatalProtocolException e)
183 + {
184 + this.Messaging.Write(ExtensionCacheWarnings.NugetException(id, e.Message));
185 + }
186 }
187
188 if (nugetVersion is null)
@@ -168,7 +193,9 @@ namespace WixToolset.Core.ExtensionCache
193
194 var searchSources = versionSource is null ? sources : new[] { versionSource };
195
171 - var extensionFolder = Path.Combine(this.CacheFolder(global), id, nugetVersion.ToString());
196 + var cacheFolder = this.GetCacheFolder(global);
197 +
198 + var extensionFolder = Path.Combine(cacheFolder, id, nugetVersion.ToString());
199
200 foreach (var source in searchSources)
201 {
@@ -183,6 +210,8 @@ namespace WixToolset.Core.ExtensionCache
210 {
211 stream.Position = 0;
212
213 + Directory.CreateDirectory(extensionFolder);
214 +
215 using (var archive = new PackageArchiveReader(stream))
216 {
217 var files = PackagingConstants.Folders.Known.SelectMany(folder => archive.GetFiles(folder)).Distinct(StringComparer.OrdinalIgnoreCase);
@@ -198,7 +227,10 @@ namespace WixToolset.Core.ExtensionCache
227 return false;
228 }
229
201 - private string ExtractProgress(string sourceFile, string targetPath, Stream fileStream) => fileStream.CopyToFile(targetPath);
230 + private string ExtractProgress(string sourceFile, string targetPath, Stream fileStream)
231 + {
232 + return fileStream.CopyToFile(targetPath);
233 + }
234
235 private static (string extensionId, string extensionVersion) ParseExtensionReference(string extensionReference)
236 {
src/wix/WixToolset.Core.ExtensionCache/ExtensionCacheManagerCommand.cs
+6 -3
@@ -25,17 +25,20 @@ namespace WixToolset.Core.ExtensionCache
25 public ExtensionCacheManagerCommand(IServiceProvider serviceProvider)
26 {
27 this.Messaging = serviceProvider.GetService<IMessaging>();
28 + this.ExtensionManager = serviceProvider.GetService<IExtensionManager>();
29 this.ExtensionReferences = new List<string>();
30 }
31
31 - private IMessaging Messaging { get; }
32 -
32 public bool ShowHelp { get; set; }
33
34 public bool ShowLogo { get; set; }
35
36 public bool StopParsing { get; set; }
37
38 + private IMessaging Messaging { get; }
39 +
40 + private IExtensionManager ExtensionManager { get; }
41 +
42 private bool Global { get; set; }
43
44 private CacheSubcommand? Subcommand { get; set; }
@@ -51,7 +54,7 @@ namespace WixToolset.Core.ExtensionCache
54 }
55
56 var success = false;
54 - var cacheManager = new ExtensionCacheManager();
57 + var cacheManager = new ExtensionCacheManager(this.Messaging, this.ExtensionManager);
58
59 switch (this.Subcommand)
60 {
src/wix/WixToolset.Core.ExtensionCache/ExtensionCacheWarnings.cs new
+24
@@ -0,0 +1,24 @@
1 +// 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.
2 +
3 +namespace WixToolset.Core.ExtensionCache
4 +{
5 + using WixToolset.Data;
6 +
7 + internal static class ExtensionCacheWarnings
8 + {
9 + public static Message NugetException(string extensionId, string exceptionMessage)
10 + {
11 + return Message(new SourceLineNumber(extensionId), Ids.NugetException, "{0}", exceptionMessage);
12 + }
13 +
14 + private static Message Message(SourceLineNumber sourceLineNumber, Ids id, string format, params object[] args)
15 + {
16 + return new Message(sourceLineNumber, MessageLevel.Warning, (int)id, format, args);
17 + }
18 +
19 + public enum Ids
20 + {
21 + NugetException = 6100,
22 + } // last available is 6499. 6500 is ExtensionCacheErrors.
23 + }
24 +}
src/wix/WixToolset.Core.ExtensionCache/WixToolsetCoreServiceProviderExtensions.cs
+4 -1
@@ -27,7 +27,10 @@ namespace WixToolset.Core.ExtensionCache
27
28 private static ExtensionCacheManager CreateExtensionCacheManager(IWixToolsetCoreServiceProvider coreProvider, Dictionary<Type, object> singletons)
29 {
30 - var extensionCacheManager = new ExtensionCacheManager();
30 + var messaging = coreProvider.GetService<IMessaging>();
31 + var extensionManager = coreProvider.GetService<IExtensionManager>();
32 +
33 + var extensionCacheManager = new ExtensionCacheManager(messaging, extensionManager);
34 singletons.Add(typeof(ExtensionCacheManager), extensionCacheManager);
35
36 return extensionCacheManager;
src/wix/WixToolset.Core.TestPackage/WixRunner.cs
+3 -1
@@ -7,6 +7,7 @@ namespace WixToolset.Core.TestPackage
7 using System.Threading;
8 using System.Threading.Tasks;
9 using WixToolset.Core.Burn;
10 + using WixToolset.Core.ExtensionCache;
11 using WixToolset.Core.WindowsInstaller;
12 using WixToolset.Data;
13 using WixToolset.Extensibility.Services;
@@ -65,7 +66,8 @@ namespace WixToolset.Core.TestPackage
66 public static Task<int> Execute(string[] args, IWixToolsetCoreServiceProvider coreProvider, out List<Message> messages, bool warningsAsErrors = true)
67 {
68 coreProvider.AddWindowsInstallerBackend()
68 - .AddBundleBackend();
69 + .AddBundleBackend()
70 + .AddExtensionCacheManager();
71
72 var listener = new TestMessageListener();
73
src/wix/WixToolset.Core.TestPackage/WixToolset.Core.TestPackage.csproj
+1
@@ -16,6 +16,7 @@
16 <ProjectReference Include="..\WixToolset.Core.Native\WixToolset.Core.Native.csproj" PrivateAssets="true" />
17 <ProjectReference Include="..\WixToolset.Core\WixToolset.Core.csproj" PrivateAssets="true" />
18 <ProjectReference Include="..\WixToolset.Core.Burn\WixToolset.Core.Burn.csproj" PrivateAssets="true" />
19 + <ProjectReference Include="..\WixToolset.Core.ExtensionCache\WixToolset.Core.ExtensionCache.csproj" PrivateAssets="true" />
20 <ProjectReference Include="..\WixToolset.Core.WindowsInstaller\WixToolset.Core.WindowsInstaller.csproj" PrivateAssets="true" />
21 </ItemGroup>
22
src/wix/WixToolset.Core/ExtensibilityServices/ExtensionCacheLocation.cs new
+19
@@ -0,0 +1,19 @@
1 +// 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.
2 +
3 +namespace WixToolset.Core.ExtensibilityServices
4 +{
5 + using WixToolset.Extensibility.Data;
6 +
7 + internal class ExtensionCacheLocation : IExtensionCacheLocation
8 + {
9 + public ExtensionCacheLocation(string path, ExtensionCacheLocationScope scope)
10 + {
11 + this.Path = path;
12 + this.Scope = scope;
13 + }
14 +
15 + public string Path { get; }
16 +
17 + public ExtensionCacheLocationScope Scope { get; }
18 + }
19 +}
src/wix/WixToolset.Core/ExtensibilityServices/ExtensionManager.cs
+35 -39
@@ -9,6 +9,7 @@ namespace WixToolset.Core.ExtensibilityServices
9 using System.Reflection;
10 using WixToolset.Data;
11 using WixToolset.Extensibility;
12 + using WixToolset.Extensibility.Data;
13 using WixToolset.Extensibility.Services;
14
15 internal class ExtensionManager : IExtensionManager
@@ -16,6 +17,7 @@ namespace WixToolset.Core.ExtensibilityServices
17 private const string UserWixFolderName = ".wix4";
18 private const string MachineWixFolderName = "WixToolset4";
19 private const string ExtensionsFolderName = "extensions";
20 + private const string UserEnvironmentName = "WIX_EXTENSIONS";
21
22 private readonly List<IExtensionFactory> extensionFactories = new List<IExtensionFactory>();
23 private readonly Dictionary<Type, List<object>> loadedExtensionsByType = new Dictionary<Type, List<object>>();
@@ -51,9 +53,14 @@ namespace WixToolset.Core.ExtensibilityServices
53 {
54 if (TryParseExtensionReference(extensionPath, out var extensionId, out var extensionVersion))
55 {
54 - foreach (var cachePath in this.CacheLocations())
56 + foreach (var cacheLocation in this.GetCacheLocations())
57 {
56 - var extensionFolder = Path.Combine(cachePath, extensionId);
58 + var extensionFolder = Path.Combine(cacheLocation.Path, extensionId);
59 +
60 + if (!Directory.Exists(extensionFolder))
61 + {
62 + continue;
63 + }
64
65 var versionFolder = extensionVersion;
66 if (String.IsNullOrEmpty(versionFolder) && !TryFindLatestVersionInFolder(extensionFolder, out versionFolder))
@@ -94,6 +101,32 @@ namespace WixToolset.Core.ExtensibilityServices
101 }
102 }
103
104 + public IReadOnlyCollection<IExtensionCacheLocation> GetCacheLocations()
105 + {
106 + var locations = new List<IExtensionCacheLocation>();
107 +
108 + var path = Path.Combine(Environment.CurrentDirectory, UserWixFolderName, ExtensionsFolderName);
109 + locations.Add(new ExtensionCacheLocation(path, ExtensionCacheLocationScope.Project));
110 +
111 + path = Environment.GetEnvironmentVariable(UserEnvironmentName) ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
112 + path = Path.Combine(path, UserWixFolderName, ExtensionsFolderName);
113 + locations.Add(new ExtensionCacheLocation(path, ExtensionCacheLocationScope.User));
114 +
115 + if (Environment.Is64BitOperatingSystem)
116 + {
117 + path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonProgramFiles), MachineWixFolderName, ExtensionsFolderName);
118 + locations.Add(new ExtensionCacheLocation(path, ExtensionCacheLocationScope.Machine));
119 + }
120 +
121 + path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonProgramFilesX86), MachineWixFolderName, ExtensionsFolderName);
122 + locations.Add(new ExtensionCacheLocation(path, ExtensionCacheLocationScope.Machine));
123 +
124 + path = Path.Combine(Path.GetDirectoryName(new Uri(Assembly.GetCallingAssembly().CodeBase).LocalPath), ExtensionsFolderName);
125 + locations.Add(new ExtensionCacheLocation(path, ExtensionCacheLocationScope.Machine));
126 +
127 + return locations;
128 + }
129 +
130 public IReadOnlyCollection<T> GetServices<T>() where T : class
131 {
132 if (!this.loadedExtensionsByType.TryGetValue(typeof(T), out var extensions))
@@ -125,43 +158,6 @@ namespace WixToolset.Core.ExtensibilityServices
158 return (IExtensionFactory)Activator.CreateInstance(type);
159 }
160
128 - private IEnumerable<string> CacheLocations()
129 - {
130 - var path = Path.Combine(Environment.CurrentDirectory, UserWixFolderName, ExtensionsFolderName);
131 - if (Directory.Exists(path))
132 - {
133 - yield return path;
134 - }
135 -
136 - path = Environment.GetEnvironmentVariable("WIX_EXTENSIONS") ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
137 - path = Path.Combine(path, UserWixFolderName, ExtensionsFolderName);
138 - if (Directory.Exists(path))
139 - {
140 - yield return path;
141 - }
142 -
143 - if (Environment.Is64BitOperatingSystem)
144 - {
145 - path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonProgramFiles), MachineWixFolderName, ExtensionsFolderName);
146 - if (Directory.Exists(path))
147 - {
148 - yield return path;
149 - }
150 - }
151 -
152 - path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonProgramFilesX86), MachineWixFolderName, ExtensionsFolderName);
153 - if (Directory.Exists(path))
154 - {
155 - yield return path;
156 - }
157 -
158 - path = Path.Combine(Path.GetDirectoryName(new Uri(Assembly.GetCallingAssembly().CodeBase).LocalPath), ExtensionsFolderName);
159 - if (Directory.Exists(path))
160 - {
161 - yield return path;
162 - }
163 - }
164 -
161 private static bool TryParseExtensionReference(string extensionReference, out string extensionId, out string extensionVersion)
162 {
163 extensionId = extensionReference ?? String.Empty;
src/wix/test/WixToolsetTest.CoreIntegration/ExtensionFixture.cs
+46
@@ -144,6 +144,52 @@ namespace WixToolsetTest.CoreIntegration
144 }
145 }
146
147 + [Fact]
148 + public void CanManipulateExtensionCache()
149 + {
150 + var currentFolder = Environment.CurrentDirectory;
151 +
152 + try
153 + {
154 + using (var fs = new DisposableFileSystem())
155 + {
156 + var folder = fs.GetFolder(true);
157 + Environment.CurrentDirectory = folder;
158 +
159 + var result = WixRunner.Execute(new[]
160 + {
161 + "extension", "add", "WixToolset.UI.wixext"
162 + });
163 +
164 + result.AssertSuccess();
165 +
166 + var cacheFolder = Path.Combine(folder, ".wix4", "extensions", "WixToolset.UI.wixext");
167 + Assert.True(Directory.Exists(cacheFolder), $"Expected folder '{cacheFolder}' to exist");
168 +
169 + result = WixRunner.Execute(new[]
170 + {
171 + "extension", "list"
172 + });
173 +
174 + result.AssertSuccess();
175 + var output = result.Messages.Select(m => m.ToString()).Single();
176 + Assert.StartsWith("WixToolset.UI.wixext 4.", output);
177 +
178 + result = WixRunner.Execute(new[]
179 + {
180 + "extension", "remove", "WixToolset.UI.wixext"
181 + });
182 +
183 + result.AssertSuccess();
184 + Assert.False(Directory.Exists(cacheFolder), $"Expected folder '{cacheFolder}' to NOT exist");
185 + }
186 + }
187 + finally
188 + {
189 + Environment.CurrentDirectory = currentFolder;
190 + }
191 + }
192 +
193 private static void Build(string[] args)
194 {
195 var result = WixRunner.Execute(args)