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)