-
Notifications
You must be signed in to change notification settings - Fork 373
/
TemplatePackageManager.cs
385 lines (349 loc) · 19.3 KB
/
TemplatePackageManager.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.TemplateEngine.Abstractions;
using Microsoft.TemplateEngine.Abstractions.TemplateFiltering;
using Microsoft.TemplateEngine.Abstractions.TemplatePackage;
using Microsoft.TemplateEngine.Edge.BuiltInManagedProvider;
namespace Microsoft.TemplateEngine.Edge.Settings
{
/// <summary>
/// Manages all <see cref="ITemplatePackageProvider"/>s available to the host.
/// Use this class to get all template packages and templates installed.
/// </summary>
public class TemplatePackageManager : IDisposable
{
private readonly IEngineEnvironmentSettings _environmentSettings;
private readonly SettingsFilePaths _paths;
private readonly ILogger _logger;
private readonly Scanner _installScanner;
private volatile TemplateCache? _userTemplateCache;
private Dictionary<ITemplatePackageProvider, Task<IReadOnlyList<ITemplatePackage>>>? _cachedSources;
/// <summary>
/// Creates the instance.
/// </summary>
/// <param name="environmentSettings">template engine environment settings.</param>
public TemplatePackageManager(IEngineEnvironmentSettings environmentSettings)
{
_environmentSettings = environmentSettings;
_logger = environmentSettings.Host.LoggerFactory.CreateLogger<TemplatePackageManager>();
_paths = new SettingsFilePaths(environmentSettings);
_installScanner = new Scanner(environmentSettings);
}
/// <summary>
/// Triggered every time when the list of <see cref="ITemplatePackage"/>s changes, this is triggered by <see cref="ITemplatePackageProvider.TemplatePackagesChanged"/>.
/// </summary>
public event Action? TemplatePackagesChanged;
/// <summary>
/// Returns <see cref="IManagedTemplatePackageProvider"/> with specified name.
/// </summary>
/// <param name="name">Name from <see cref="ITemplatePackageProviderFactory.DisplayName"/>.</param>
/// <returns></returns>
/// <remarks>For default built-in providers use <see cref="GetBuiltInManagedProvider"/> method instead.</remarks>
public IManagedTemplatePackageProvider GetManagedProvider(string name)
{
EnsureProvidersLoaded();
return _cachedSources!.Keys.OfType<IManagedTemplatePackageProvider>().FirstOrDefault(p => p.Factory.DisplayName == name);
}
/// <summary>
/// Returns <see cref="IManagedTemplatePackageProvider"/> with specified <see cref="Guid"/>.
/// </summary>
/// <param name="id"><see cref="Guid"/> from <see cref="IIdentifiedComponent.Id"/> of <see cref="ITemplatePackageProviderFactory"/>.</param>
/// <returns></returns>
/// <remarks>For default built-in providers use <see cref="GetBuiltInManagedProvider"/> method instead.</remarks>
public IManagedTemplatePackageProvider GetManagedProvider(Guid id)
{
EnsureProvidersLoaded();
return _cachedSources!.Keys.OfType<IManagedTemplatePackageProvider>().FirstOrDefault(p => p.Factory.Id == id);
}
/// <summary>
/// Same as <see cref="GetTemplatePackagesAsync"/> but filters only <see cref="IManagedTemplatePackage"/> packages.
/// </summary>
/// <param name="force">Useful when <see cref="IManagedTemplatePackage"/> doesn't trigger <see cref="ITemplatePackageProvider.TemplatePackagesChanged"/> event.</param>
/// <param name="cancellationToken">A cancellation token to cancel the asynchronous operation.</param>
/// <returns>The list of <see cref="IManagedTemplatePackage"/>.</returns>
public async Task<IReadOnlyList<IManagedTemplatePackage>> GetManagedTemplatePackagesAsync(bool force, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
EnsureProvidersLoaded();
return (await GetTemplatePackagesAsync(force, cancellationToken).ConfigureAwait(false)).OfType<IManagedTemplatePackage>().ToList();
}
/// <summary>
/// Returns combined list of <see cref="ITemplatePackage"/>s that all <see cref="ITemplatePackageProvider"/>s and <see cref="IManagedTemplatePackageProvider"/>s return.
/// <see cref="TemplatePackageManager"/> caches the responses from <see cref="ITemplatePackageProvider"/>s, to get non-cached response <paramref name="force"/> should be set to true.
/// Note that specifying <paramref name="force"/> will only return responses from already loaded providers. To reload providers, instantiate new instance of the <see cref="TemplatePackageManager"/>.
/// </summary>
/// <param name="force">Useful when <see cref="ITemplatePackageProvider"/> doesn't trigger <see cref="ITemplatePackageProvider.TemplatePackagesChanged"/> event.</param>
/// <param name="cancellationToken">A cancellation token to cancel the asynchronous operation.</param>
/// <returns>The list of <see cref="ITemplatePackage"/>s.</returns>
public async Task<IReadOnlyList<ITemplatePackage>> GetTemplatePackagesAsync(bool force, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
EnsureProvidersLoaded();
if (force)
{
foreach (var provider in _cachedSources!.Keys.ToList())
{
_cachedSources[provider] = Task.Run(() => provider.GetAllTemplatePackagesAsync(default));
}
}
var sources = new List<ITemplatePackage>();
foreach (KeyValuePair<ITemplatePackageProvider, Task<IReadOnlyList<ITemplatePackage>>> source in _cachedSources.OrderBy((p) => (p.Key.Factory as IPrioritizedComponent)?.Priority ?? 0))
{
try
{
sources.AddRange(await source.Value.ConfigureAwait(false));
}
catch (Exception ex)
{
_logger.LogError(LocalizableStrings.TemplatePackageManager_Error_FailedToGetTemplatePackages, source.Key.Factory.DisplayName, ex.Message);
_logger.LogDebug("Details: {0}", ex);
}
}
return sources;
}
public void Dispose()
{
if (_cachedSources == null)
{
return;
}
foreach (var provider in _cachedSources.Keys.OfType<IDisposable>())
{
provider.Dispose();
}
}
/// <summary>
/// Returns built-in <see cref="IManagedTemplatePackageProvider"/> of specified <see cref="InstallationScope"/>.
/// </summary>
/// <param name="scope">scope managed by built-in provider.</param>
/// <returns><see cref="IManagedTemplatePackageProvider"/> which manages packages of <paramref name="scope"/>.</returns>
public IManagedTemplatePackageProvider GetBuiltInManagedProvider(InstallationScope scope = InstallationScope.Global)
{
switch (scope)
{
case InstallationScope.Global:
return GetManagedProvider(GlobalSettingsTemplatePackageProviderFactory.FactoryId);
default:
break;
}
return GetManagedProvider(GlobalSettingsTemplatePackageProviderFactory.FactoryId);
}
/// <summary>
/// Gets all templates based on current settings.
/// </summary>
/// <remarks>
/// This call is cached. And can be invalidated by <see cref="RebuildTemplateCacheAsync"/>.
/// </remarks>
public async Task<IReadOnlyList<ITemplateInfo>> GetTemplatesAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var userTemplateCache = await UpdateTemplateCacheAsync(false, cancellationToken).ConfigureAwait(false);
return userTemplateCache.TemplateInfo;
}
/// <summary>
/// Gets the templates filtered using <paramref name="filters"/> and <paramref name="matchFilter"/>.
/// </summary>
/// <param name="matchFilter">The criteria for <see cref="ITemplateMatchInfo"/> to be included to result collection.</param>
/// <param name="filters">The list of filters to be applied to templates.</param>
/// <param name="cancellationToken">A cancellation token to cancel the asynchronous operation.</param>
/// <returns>The filtered list of templates with match information.</returns>
/// <example>
/// <c>GetTemplatesAsync(WellKnownSearchFilters.MatchesAllCriteria, new [] { WellKnownSearchFilters.NameFilter("myname") }</c> - returns the templates which name or short name contains "myname". <br/>
/// <c>GetTemplatesAsync(TemplateListFilter.MatchesAtLeastOneCriteria, new [] { WellKnownSearchFilters.NameFilter("myname"), WellKnownSearchFilters.NameFilter("othername") })</c> - returns the templates which name or short name contains "myname" or "othername".<br/>
/// </example>
public async Task<IReadOnlyList<ITemplateMatchInfo>> GetTemplatesAsync(Func<ITemplateMatchInfo, bool> matchFilter, IEnumerable<Func<ITemplateInfo, MatchInfo?>> filters, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
IReadOnlyList<ITemplateInfo> templates = await GetTemplatesAsync(cancellationToken).ConfigureAwait(false);
//TemplateListFilter.GetTemplateMatchInfo code should be moved to this method eventually, when no longer needed.
#pragma warning disable CS0618 // Type or member is obsolete.
return TemplateListFilter.GetTemplateMatchInfo(templates, matchFilter, filters.ToArray()).ToList();
#pragma warning restore CS0618 // Type or member is obsolete
}
/// <summary>
/// Deletes templates cache and rebuilds it.
/// Useful if user suspects cache is corrupted and wants to rebuild it.
/// </summary>
public Task RebuildTemplateCacheAsync(CancellationToken token)
{
token.ThrowIfCancellationRequested();
return UpdateTemplateCacheAsync(true, token);
}
/// <summary>
/// Helper method that returns <see cref="ITemplatePackage"/> that contains <paramref name="template"/>.
/// </summary>
public async Task<ITemplatePackage> GetTemplatePackageAsync(ITemplateInfo template, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
IReadOnlyList<ITemplatePackage> templatePackages = await GetTemplatePackagesAsync(false, cancellationToken).ConfigureAwait(false);
return templatePackages.Single(s => s.MountPointUri == template.MountPointUri);
}
/// <summary>
/// Returns all <see cref="ITemplateInfo"/> contained by <paramref name="templatePackage"/>.
/// </summary>
/// <param name="templatePackage">The template package to get template from.</param>
/// <param name="cancellationToken">A cancellation token to cancel the asynchronous operation.</param>
/// <returns>The enumerator to templates of the <paramref name="templatePackage"/>.</returns>
public async Task<IEnumerable<ITemplateInfo>> GetTemplatesAsync(ITemplatePackage templatePackage, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var allTemplates = await GetTemplatesAsync(cancellationToken).ConfigureAwait(false);
return allTemplates.Where(t => t.MountPointUri == templatePackage.MountPointUri);
}
/// <summary>
/// Returns managed template package <see cref="IManagedTemplatePackage"/> matching <paramref name="packageIdentifier"/> and containing templates <see cref="ITemplateInfo"/>.
/// </summary>
/// <param name="packageIdentifier">The template package identifier.</param>
/// <param name="packageVersion">The template package version, if null package version is not checked.</param>
/// <param name="cancellationToken">A cancellation token to cancel the asynchronous operation.</param>
/// <returns>The managed template package and the containing templates.</returns>
/// <exception cref="InvalidOperationException"> Throws an exception when package <paramref name="packageIdentifier"/>.</exception>
public async Task<(IManagedTemplatePackage? Package, IEnumerable<ITemplateInfo>? Templates)> GetManagedTemplatePackageAsync(string packageIdentifier, string? packageVersion = null, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var templatePackages = await GetManagedTemplatePackagesAsync(false, cancellationToken).ConfigureAwait(false);
var foundPackage = templatePackages
.FirstOrDefault(tp =>
{
if (tp?.Identifier == packageIdentifier)
{
return string.IsNullOrWhiteSpace(packageVersion) || tp.Version == packageVersion;
}
return false;
});
if (foundPackage != null)
{
var templates = await GetTemplatesAsync(foundPackage, cancellationToken).ConfigureAwait(false);
return (foundPackage, templates);
}
throw new InvalidOperationException(string.Format(LocalizableStrings.TemplatePackageManager_Error_FailedToFindPackage, packageIdentifier));
}
private void EnsureProvidersLoaded()
{
if (_cachedSources != null)
{
return;
}
_cachedSources = new Dictionary<ITemplatePackageProvider, Task<IReadOnlyList<ITemplatePackage>>>();
var providers = _environmentSettings.Components.OfType<ITemplatePackageProviderFactory>().Select(f => f.CreateProvider(_environmentSettings));
foreach (var provider in providers)
{
provider.TemplatePackagesChanged += () =>
{
_cachedSources[provider] = provider.GetAllTemplatePackagesAsync(default);
TemplatePackagesChanged?.Invoke();
};
_cachedSources[provider] = Task.Run(() => provider.GetAllTemplatePackagesAsync(default));
}
}
private async Task<TemplateCache> UpdateTemplateCacheAsync(bool needsRebuild, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
// Kick off gathering template packages, so parsing cache can happen in parallel.
Task<IReadOnlyList<ITemplatePackage>> getTemplatePackagesTask = GetTemplatePackagesAsync(needsRebuild, cancellationToken);
if (_userTemplateCache is not TemplateCache cache)
{
try
{
_userTemplateCache = cache = new TemplateCache(_environmentSettings.Host.FileSystem.ReadObject(_paths.TemplateCacheFile));
}
catch (FileNotFoundException)
{
// Don't log this, it's expected, we just don't want to do File.Exists...
cache = new TemplateCache(null);
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Failed to load templatecache.json.");
cache = new TemplateCache(null);
}
}
if (cache.Version == null)
{
// Null version means, parsing cache failed.
needsRebuild = true;
}
if (!needsRebuild && cache.Version != TemplateInfo.CurrentVersion)
{
_logger.LogDebug($"Template cache file version is {cache.Version}, but template engine is {TemplateInfo.CurrentVersion}, rebuilding cache.");
needsRebuild = true;
}
if (!needsRebuild && cache.Locale != CultureInfo.CurrentUICulture.Name)
{
_logger.LogDebug($"Template cache locale is {cache.Locale}, but CurrentUICulture is {CultureInfo.CurrentUICulture.Name}, rebuilding cache.");
needsRebuild = true;
}
var allTemplatePackages = await getTemplatePackagesTask.ConfigureAwait(false);
var mountPoints = new Dictionary<string, DateTime>();
foreach (var package in allTemplatePackages)
{
mountPoints[package.MountPointUri] = package.LastChangeTime;
// We can stop comparing, but we need to keep looping to fill mountPoints
if (!needsRebuild)
{
if (cache.MountPointsInfo.TryGetValue(package.MountPointUri, out var cachedLastChangeTime))
{
if (package.LastChangeTime > cachedLastChangeTime)
{
needsRebuild = true;
}
}
else
{
needsRebuild = true;
}
}
}
cancellationToken.ThrowIfCancellationRequested();
// Check that some mountpoint wasn't removed...
if (!needsRebuild && !mountPoints.Keys.OrderBy(mp => mp).SequenceEqual(cache.MountPointsInfo.Keys.OrderBy(mp => mp)))
{
needsRebuild = true;
}
// Cool, looks like everything is up to date, exit
if (!needsRebuild)
{
return cache;
}
var scanResults = new ScanResult?[allTemplatePackages.Count];
Parallel.For(0, allTemplatePackages.Count, async (int index) =>
{
try
{
var scanResult = await _installScanner.ScanAsync(allTemplatePackages[index].MountPointUri, cancellationToken).ConfigureAwait(false);
scanResults[index] = scanResult;
}
catch (Exception ex)
{
_logger.LogWarning(LocalizableStrings.TemplatePackageManager_Error_FailedToScan, allTemplatePackages[index].MountPointUri, ex.Message);
_logger.LogDebug($"Stack trace: {ex.StackTrace}");
}
});
cancellationToken.ThrowIfCancellationRequested();
cache = new TemplateCache(allTemplatePackages, scanResults, mountPoints, _environmentSettings);
foreach (var scanResult in scanResults)
{
scanResult?.Dispose();
}
_userTemplateCache = cache;
try
{
_environmentSettings.Host.FileSystem.WriteObject(_paths.TemplateCacheFile, cache);
}
catch (Exception ex)
{
_logger.LogWarning(LocalizableStrings.TemplatePackageManager_Error_FailedToStoreCache, ex.Message);
_logger.LogDebug($"Stack trace: {ex.StackTrace}");
}
return cache;
}
}
}