Add better support for zip extensions (#607)

- AssemblyLoadContext requires assemblies to be seekable, so we now will copy assemblies to byte arrays if they are not seekable
- We check for pattern that occurs when a zip file is created from a folder where the top level directory is a simple folder

This change also consolidates loading logic all into the assembly load context where it will attempt to load the assemblies from the manifest.
This commit is contained in:
Taylor Southwick 2021-06-08 11:53:53 -07:00 коммит произвёл GitHub
Родитель 060d1d22f1
Коммит 29580acdc5
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
6 изменённых файлов: 140 добавлений и 97 удалений

Просмотреть файл

@ -2,9 +2,11 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using Microsoft.Extensions.FileProviders;
namespace Microsoft.DotNet.UpgradeAssistant.Extensions
{
@ -14,10 +16,38 @@ namespace Microsoft.DotNet.UpgradeAssistant.Extensions
private readonly ExtensionInstance _extension;
public ExtensionAssemblyLoadContext(ExtensionInstance extension)
public ExtensionAssemblyLoadContext(ExtensionInstance extension, string[] assemblies)
: base(ALC_Prefix + extension.Name)
{
_extension = extension;
Load(extension, assemblies);
}
private void Load(ExtensionInstance extension, string[] assemblies)
{
foreach (var path in assemblies)
{
try
{
var fileInfo = extension.FileProvider.GetFileInfo(path);
if (!fileInfo.Exists)
{
Console.WriteLine($"ERROR: Could not find extension service provider assembly {path} in extension {extension.Name}");
continue;
}
using var assemblyStream = GetSeekableStream(fileInfo);
LoadFromStream(assemblyStream);
}
catch (FileLoadException)
{
}
catch (BadImageFormatException)
{
}
}
}
protected override Assembly? Load(AssemblyName assemblyName)
@ -35,14 +65,14 @@ namespace Microsoft.DotNet.UpgradeAssistant.Extensions
if (dllFile.Exists)
{
using var dllStream = dllFile.CreateReadStream();
using var dllStream = GetSeekableStream(dllFile);
var pdb = $"{assemblyName.Name}.pdb";
var pdbFile = _extension.FileProvider.GetFileInfo(pdb);
if (pdbFile.Exists)
{
using var pdbStream = pdbFile.CreateReadStream();
using var pdbStream = GetSeekableStream(pdbFile);
return LoadFromStream(dllStream, pdbStream);
}
else
@ -53,5 +83,21 @@ namespace Microsoft.DotNet.UpgradeAssistant.Extensions
return null;
}
private static Stream GetSeekableStream(IFileInfo file)
{
var assemblyStream = file.CreateReadStream();
if (assemblyStream.CanSeek)
{
return assemblyStream;
}
var ms = new MemoryStream();
assemblyStream.CopyTo(ms);
ms.Position = 0;
assemblyStream.Dispose();
return ms;
}
}
}

Просмотреть файл

@ -2,21 +2,26 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.Loader;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.FileProviders;
namespace Microsoft.DotNet.UpgradeAssistant.Extensions
{
[DebuggerDisplay("{Name}, {Location}")]
public sealed class ExtensionInstance : IDisposable
{
private const string ExtensionServiceProvidersSectionName = "ExtensionServiceProviders";
public const string ManifestFileName = "ExtensionManifest.json";
private const string ExtensionNamePropertyName = "ExtensionName";
private const string DefaultExtensionName = "Unknown";
private readonly Lazy<AssemblyLoadContext> _alc;
private readonly Lazy<AssemblyLoadContext>? _alc;
public ExtensionInstance(IFileProvider fileProvider, string location)
{
@ -24,18 +29,30 @@ namespace Microsoft.DotNet.UpgradeAssistant.Extensions
Location = location;
Configuration = CreateConfiguration(fileProvider);
Name = GetName(Configuration, location);
_alc = new Lazy<AssemblyLoadContext>(() => new ExtensionAssemblyLoadContext(this));
var serviceProviders = GetOptions<string[]>(ExtensionServiceProvidersSectionName);
if (serviceProviders is not null)
{
_alc = new Lazy<AssemblyLoadContext>(() => new ExtensionAssemblyLoadContext(this, serviceProviders));
}
}
public string Name { get; }
public bool HasAssemblyLoadContext => _alc.IsValueCreated;
public IEnumerable<IExtensionServiceProvider> GetServiceProviders()
{
if (_alc is null)
{
return Enumerable.Empty<IExtensionServiceProvider>();
}
/// <summary>
/// Gets the <see cref="AssemblyLoadContext"/> for the extension. Guard calls with <see cref="HasAssemblyLoadContext"/> first,
/// otherwise it may trigger creation of the load context if it is not needed.
/// </summary>
public AssemblyLoadContext LoadContext => _alc.Value;
return _alc.Value.Assemblies.SelectMany(assembly => assembly
.GetTypes()
.Where(t => t.IsPublic && !t.IsAbstract && typeof(IExtensionServiceProvider).IsAssignableFrom(t))
.Select(t => Activator.CreateInstance(t))
.Cast<IExtensionServiceProvider>());
}
public string Location { get; }
@ -74,7 +91,20 @@ namespace Microsoft.DotNet.UpgradeAssistant.Extensions
{
var provider = new ZipFileProvider(e);
return new ExtensionInstance(new ZipFileProvider(e), e);
try
{
return new ExtensionInstance(provider, e);
}
// If the manifest file couldn't be found, let's try looking at one layer deep with the name
// of the file as the first folder. This is what happens when you create a zip file from a folder
// with Windows or 7-zip
catch (UpgradeException ex) when (ex.InnerException is FileNotFoundException)
{
var subpath = Path.GetFileNameWithoutExtension(e);
var subprovider = new SubFileProvider(provider, subpath);
return new ExtensionInstance(subprovider, e);
}
}
}
else
@ -106,8 +136,17 @@ namespace Microsoft.DotNet.UpgradeAssistant.Extensions
}
private static IConfiguration CreateConfiguration(IFileProvider fileProvider)
=> new ConfigurationBuilder()
.AddJsonFile(fileProvider, ManifestFileName, optional: false, reloadOnChange: false)
.Build();
{
try
{
return new ConfigurationBuilder()
.AddJsonFile(fileProvider, ManifestFileName, optional: false, reloadOnChange: false)
.Build();
}
catch (FileNotFoundException e)
{
throw new UpgradeException("Could not find manifest file", e);
}
}
}
}

Просмотреть файл

@ -82,26 +82,5 @@ namespace Microsoft.DotNet.UpgradeAssistant.Extensions
return Array.Empty<TTo>();
}
}
private class SubFileProvider : IFileProvider
{
private readonly IFileProvider _other;
private readonly string _path;
public SubFileProvider(IFileProvider other, string path)
{
_other = other;
_path = path;
}
public IDirectoryContents GetDirectoryContents(string subpath)
=> _other.GetDirectoryContents(Path.Combine(_path, subpath));
public IFileInfo GetFileInfo(string subpath)
=> _other.GetFileInfo(Path.Combine(_path, subpath));
public IChangeToken Watch(string filter)
=> _other.Watch(Path.Combine(_path, filter));
}
}
}

Просмотреть файл

@ -14,7 +14,6 @@ namespace Microsoft.DotNet.UpgradeAssistant.Extensions
{
public static class ExtensionProviderExtensions
{
private const string ExtensionServiceProvidersSectionName = "ExtensionServiceProviders";
private const string UpgradeAssistantExtensionPathsSettingName = "UpgradeAssistantExtensionPaths";
/// <summary>
@ -41,7 +40,12 @@ namespace Microsoft.DotNet.UpgradeAssistant.Extensions
foreach (var extension in GetExtensions(configuration, additionalExtensionPaths))
{
services.AddExtension(extension);
services.AddSingleton(extension);
foreach (var sp in extension.GetServiceProviders())
{
sp.AddServices(new ExtensionServiceCollection(services, extension));
}
}
}
@ -86,63 +90,5 @@ namespace Microsoft.DotNet.UpgradeAssistant.Extensions
return fromConfig.Concat(pathsFromString).Concat(additionalExtensionPaths);
}
}
private static void AddExtension(this IServiceCollection services, ExtensionInstance extension)
{
services.AddSingleton(extension);
var extensionServiceProviderPaths = extension.GetOptions<string[]>(ExtensionServiceProvidersSectionName);
if (extensionServiceProviderPaths is null)
{
return;
}
foreach (var path in extensionServiceProviderPaths)
{
try
{
var fileInfo = extension.FileProvider.GetFileInfo(path);
if (!fileInfo.Exists)
{
Console.WriteLine($"ERROR: Could not find extension service provider assembly {path} in extension {extension.Name}");
continue;
}
using var assemblyStream = fileInfo.CreateReadStream();
if (assemblyStream is null)
{
Console.WriteLine($"ERROR: Could not find extension service provider assembly {path} in extension {extension.Name}");
continue;
}
extension.LoadContext.LoadFromStream(assemblyStream);
}
catch (FileLoadException)
{
}
catch (BadImageFormatException)
{
}
}
if (!extension.HasAssemblyLoadContext)
{
return;
}
var serviceProviders = extension.LoadContext.Assemblies.SelectMany(assembly => assembly
.GetTypes()
.Where(t => t.IsPublic && !t.IsAbstract && typeof(IExtensionServiceProvider).IsAssignableFrom(t))
.Select(t => Activator.CreateInstance(t))
.Cast<IExtensionServiceProvider>());
foreach (var sp in serviceProviders)
{
sp.AddServices(new ExtensionServiceCollection(services, extension));
}
}
}
}

Просмотреть файл

@ -0,0 +1,30 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.IO;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;
namespace Microsoft.DotNet.UpgradeAssistant.Extensions
{
internal sealed class SubFileProvider : IFileProvider
{
private readonly IFileProvider _other;
private readonly string _path;
public SubFileProvider(IFileProvider other, string path)
{
_other = other;
_path = path;
}
public IDirectoryContents GetDirectoryContents(string subpath)
=> _other.GetDirectoryContents(Path.Combine(_path, subpath));
public IFileInfo GetFileInfo(string subpath)
=> _other.GetFileInfo(Path.Combine(_path, subpath));
public IChangeToken Watch(string filter)
=> _other.Watch(Path.Combine(_path, filter));
}
}

Просмотреть файл

@ -36,6 +36,7 @@ namespace Microsoft.DotNet.UpgradeAssistant.Extensions
public IDirectoryContents GetDirectoryContents(string subpath)
{
subpath = NormalizeZipPath(subpath);
var list = new List<IFileInfo>();
foreach (var entry in _archive.Entries)
@ -56,9 +57,11 @@ namespace Microsoft.DotNet.UpgradeAssistant.Extensions
return new ListDirectoryContents(list);
}
private static string NormalizeZipPath(string path) => path.Replace('\\', '/');
public IFileInfo GetFileInfo(string subpath)
{
var entry = _archive.GetEntry(subpath);
var entry = _archive.GetEntry(NormalizeZipPath(subpath));
if (entry is null)
{