Use new InMemoryFileProvider behaviours to clean up ReferencedAssemblyFileProvider
This commit is contained in:
parent
867cb66b97
commit
5ec2a5eb1e
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.Blazor.Mono;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -8,18 +9,33 @@ namespace Microsoft.Blazor.Server.Test
|
|||
{
|
||||
public class ReferencedAssemblyFileProviderTest
|
||||
{
|
||||
[Fact]
|
||||
public void RootDirContainsOnlyBinDir()
|
||||
{
|
||||
var provider = new ReferencedAssemblyFileProvider(
|
||||
typeof (HostedInAspNet.Client.Program).Assembly,
|
||||
MonoStaticFileProvider.Instance);
|
||||
Assert.Collection(provider.GetDirectoryContents("/"), item =>
|
||||
{
|
||||
Assert.Equal("/bin", item.PhysicalPath);
|
||||
Assert.True(item.IsDirectory);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindsEntrypointAssemblyAndReferencedAssemblies()
|
||||
{
|
||||
var provider = new ReferencedAssemblyFileProvider<HostedInAspNet.Client.Program>();
|
||||
var contents = provider.GetDirectoryContents(string.Empty).OrderBy(i => i.Name).ToList();
|
||||
var provider = new ReferencedAssemblyFileProvider(
|
||||
typeof(HostedInAspNet.Client.Program).Assembly,
|
||||
MonoStaticFileProvider.Instance);
|
||||
var contents = provider.GetDirectoryContents("/bin").OrderBy(i => i.Name).ToList();
|
||||
Assert.Collection(contents,
|
||||
item => { Assert.Equal("HostedInAspNet.Client.dll", item.PhysicalPath); },
|
||||
item => { Assert.Equal("mscorlib.dll", item.PhysicalPath); },
|
||||
item => { Assert.Equal("System.Console.dll", item.PhysicalPath); },
|
||||
item => { Assert.Equal("System.Core.dll", item.PhysicalPath); },
|
||||
item => { Assert.Equal("System.dll", item.PhysicalPath); },
|
||||
item => { Assert.Equal("System.Runtime.dll", item.PhysicalPath); });
|
||||
item => { Assert.Equal("/bin/HostedInAspNet.Client.dll", item.PhysicalPath); },
|
||||
item => { Assert.Equal("/bin/mscorlib.dll", item.PhysicalPath); },
|
||||
item => { Assert.Equal("/bin/System.Console.dll", item.PhysicalPath); },
|
||||
item => { Assert.Equal("/bin/System.Core.dll", item.PhysicalPath); },
|
||||
item => { Assert.Equal("/bin/System.dll", item.PhysicalPath); },
|
||||
item => { Assert.Equal("/bin/System.Runtime.dll", item.PhysicalPath); });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ namespace Microsoft.Blazor.Internal.Common.FileProviders
|
|||
{
|
||||
dataStream.CopyTo(ms);
|
||||
_fileData = ms.ToArray();
|
||||
dataStream.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,168 +1,155 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
using Mono.Cecil;
|
||||
using Microsoft.Blazor.Mono;
|
||||
using Microsoft.Blazor.Internal.Common.FileProviders;
|
||||
using System.Reflection;
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Blazor.Server
|
||||
{
|
||||
internal class ReferencedAssemblyFileProvider<TApp> : IFileProvider
|
||||
internal class ReferencedAssemblyFileProvider : InMemoryFileProvider
|
||||
{
|
||||
private static readonly Dictionary<string, IFileInfo> _frameworkDlls = ReadFrameworkDlls();
|
||||
private readonly Contents _referencedAssemblyContents = new Contents(FindReferencedAssemblies());
|
||||
private readonly Contents _emptyContents = new Contents(null);
|
||||
|
||||
public IDirectoryContents GetDirectoryContents(string subpath)
|
||||
public ReferencedAssemblyFileProvider(Assembly entrypointAssembly, IFileProvider clientBcl)
|
||||
: base(ComputeContents(entrypointAssembly, clientBcl))
|
||||
{
|
||||
return subpath == string.Empty
|
||||
? _referencedAssemblyContents
|
||||
: _emptyContents;
|
||||
}
|
||||
|
||||
public IFileInfo GetFileInfo(string subpath)
|
||||
private static IEnumerable<(string, Stream)> ComputeContents(
|
||||
Assembly entrypointAssembly,
|
||||
IFileProvider clientBcl)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public IChangeToken Watch(string filter)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
private static IEnumerable<AssemblyDefinition> FindReferencedAssemblies()
|
||||
{
|
||||
var foundAssemblies = new Dictionary<string, AssemblyDefinition>();
|
||||
FindReferencedAssembliesRecursive(
|
||||
AssemblyDefinition.ReadAssembly(typeof(TApp).Assembly.Location),
|
||||
var foundAssemblies = new Dictionary<string, ReferencedAssembly>();
|
||||
AddWithReferencesRecursive(
|
||||
new ReferencedAssembly(AssemblyDefinition.ReadAssembly(entrypointAssembly.Location)),
|
||||
clientBcl,
|
||||
foundAssemblies);
|
||||
return foundAssemblies.Values;
|
||||
|
||||
return foundAssemblies.Values.Select(assembly => (
|
||||
$"/bin/{assembly.Name}.dll",
|
||||
(Stream)new MemoryStream(assembly.Data)));
|
||||
}
|
||||
|
||||
private static void FindReferencedAssembliesRecursive(AssemblyDefinition root, IDictionary<string, AssemblyDefinition> results)
|
||||
private static void AddWithReferencesRecursive(
|
||||
ReferencedAssembly root,
|
||||
IFileProvider clientBcl,
|
||||
IDictionary<string, ReferencedAssembly> results)
|
||||
{
|
||||
results.Add(root.Name.Name, root);
|
||||
|
||||
foreach (var module in root.Modules)
|
||||
results.Add(root.Name, root);
|
||||
|
||||
foreach (var module in root.Definition.Modules)
|
||||
{
|
||||
foreach (var referenceName in module.AssemblyReferences)
|
||||
{
|
||||
if (!results.ContainsKey(referenceName.Name))
|
||||
{
|
||||
var resolvedReference = FindAssembly(referenceName, module.AssemblyResolver);
|
||||
|
||||
// Some of the referenced assemblies aren't included in the Mono BCL, e.g.,
|
||||
// Mono.Security.dll which is referenced from System.dll. These ones are not
|
||||
// required at runtime, so just skip them.
|
||||
var resolvedReference = ResolveReference(clientBcl, module, referenceName);
|
||||
if (resolvedReference != null)
|
||||
{
|
||||
FindReferencedAssembliesRecursive(resolvedReference, results);
|
||||
AddWithReferencesRecursive(resolvedReference, clientBcl, results);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static AssemblyDefinition FindAssembly(AssemblyNameReference name, IAssemblyResolver nativeResolver)
|
||||
private static ReferencedAssembly ResolveReference(IFileProvider clientBcl, ModuleDefinition module, AssemblyNameReference referenceName)
|
||||
{
|
||||
try
|
||||
if (SearchInFileProvider(clientBcl, string.Empty, $"{referenceName.Name}.dll", out var bclFile))
|
||||
{
|
||||
return _frameworkDlls.TryGetValue($"{name.Name}.dll", out var fileInfo)
|
||||
? AssemblyDefinition.ReadAssembly(fileInfo.CreateReadStream())
|
||||
: AllowNativeDllResolution(name) ? nativeResolver.Resolve(name) : null;
|
||||
// Where possible, we resolve references to client BCL assemblies
|
||||
return new ReferencedAssembly(bclFile);
|
||||
}
|
||||
catch (AssemblyResolutionException)
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool AllowNativeDllResolution(AssemblyNameReference name)
|
||||
{
|
||||
// System.* assemblies must only be resolved from the browser-reachable FrameworkFiles.
|
||||
// It's no use resolving them using the native resolver, because those files wouldn't
|
||||
// be accessible at runtime anyway.
|
||||
return !name.Name.StartsWith("System.", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static Dictionary<string, IFileInfo> ReadFrameworkDlls()
|
||||
{
|
||||
// TODO: Stop leaking knowledge of the Microsoft.Blazor.Mono file provider internal
|
||||
// structure into this unrelated class. Currently it's needed because that file provider
|
||||
// doesn't support proper directory hierarchies and therefore keeps all files in the
|
||||
// top-level directory, putting '$' into filenames in place of directories.
|
||||
// To fix this, make MonoStaticFileProvider expose a regular directory structure, and
|
||||
// then change this method to walk it recursively.
|
||||
|
||||
return MonoStaticFileProvider.Instance
|
||||
.GetDirectoryContents(string.Empty)
|
||||
.Where(file => file.Name.EndsWith(".dll", StringComparison.Ordinal))
|
||||
.ToDictionary(MonoEmbeddedResourceToFilename, file => file);
|
||||
|
||||
string MonoEmbeddedResourceToFilename(IFileInfo fileInfo)
|
||||
{
|
||||
var name = fileInfo.Name;
|
||||
var lastDirSeparatorPos = name.LastIndexOf('$');
|
||||
return name.Substring(lastDirSeparatorPos + 1);
|
||||
}
|
||||
}
|
||||
|
||||
private class Contents : IDirectoryContents
|
||||
{
|
||||
private readonly bool _exists;
|
||||
private readonly IReadOnlyDictionary<string, IFileInfo> _items;
|
||||
|
||||
public Contents(IEnumerable<AssemblyDefinition> assemblies)
|
||||
{
|
||||
_exists = assemblies != null;
|
||||
_items = (assemblies ?? Enumerable.Empty<AssemblyDefinition>())
|
||||
.Select(assembly => new AssemblyFileInfo(assembly))
|
||||
.ToDictionary(item => item.Name, item => (IFileInfo)item);
|
||||
}
|
||||
|
||||
public bool Exists => _exists;
|
||||
|
||||
public IEnumerator<IFileInfo> GetEnumerator() => _items.Values.GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => _items.Values.GetEnumerator();
|
||||
}
|
||||
|
||||
private class AssemblyFileInfo : IFileInfo
|
||||
{
|
||||
private readonly byte[] _data;
|
||||
|
||||
public bool Exists => true;
|
||||
|
||||
public long Length => _data.Length;
|
||||
|
||||
public string PhysicalPath => Name;
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public DateTimeOffset LastModified => default(DateTimeOffset);
|
||||
|
||||
public bool IsDirectory => false;
|
||||
|
||||
public Stream CreateReadStream()
|
||||
{
|
||||
return new MemoryStream(_data);
|
||||
}
|
||||
|
||||
public AssemblyFileInfo(AssemblyDefinition assembly)
|
||||
{
|
||||
Name = $"{assembly.Name.Name}.dll";
|
||||
|
||||
using (var ms = new MemoryStream())
|
||||
try
|
||||
{
|
||||
assembly.Write(ms);
|
||||
_data = ms.GetBuffer();
|
||||
// If it's not a client BCL assembly, maybe we can resolve it natively
|
||||
// (e.g., if it's in the app's bin directory, or a NuGet package)
|
||||
var nativelyResolved = module.AssemblyResolver.Resolve(referenceName);
|
||||
return AllowServingAssembly(nativelyResolved)
|
||||
? new ReferencedAssembly(nativelyResolved)
|
||||
: null;
|
||||
}
|
||||
catch (AssemblyResolutionException)
|
||||
{
|
||||
// Some of the referenced assemblies aren't included in the Mono BCL, e.g.,
|
||||
// Mono.Security.dll which is referenced from System.dll. These ones are not
|
||||
// required at runtime, so just skip them.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool AllowServingAssembly(AssemblyDefinition nativelyResolvedAssembly)
|
||||
{
|
||||
// When we use the native assembly resolver, it might return something from a NuGet
|
||||
// packages folder which we do want to serve, or it might return something from the
|
||||
// core .NET BCL which we *don't* want to serve (because the core BCL assemblies
|
||||
// should come from the Mono WASM distribution only). Currently there isn't a good
|
||||
// way to differentiate these cases, so as a temporary heuristic, assume anything
|
||||
// named System.* shouldn't be resolved natively.
|
||||
return !nativelyResolvedAssembly.MainModule.Name.StartsWith(
|
||||
"System.",
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool SearchInFileProvider(IFileProvider fileProvider, string searchRootDirNoTrailingSlash, string name, out IFileInfo file)
|
||||
{
|
||||
var possibleFullPath = $"{searchRootDirNoTrailingSlash}/{name}";
|
||||
var possibleResult = fileProvider.GetFileInfo(possibleFullPath);
|
||||
if (possibleResult.Exists)
|
||||
{
|
||||
file = possibleResult;
|
||||
return true;
|
||||
}
|
||||
|
||||
var childDirs = fileProvider.GetDirectoryContents(searchRootDirNoTrailingSlash)
|
||||
.Where(item => item.IsDirectory);
|
||||
foreach (var childDir in childDirs)
|
||||
{
|
||||
if (SearchInFileProvider(fileProvider, childDir.PhysicalPath, name, out file))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
file = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private class ReferencedAssembly
|
||||
{
|
||||
public string Name { get; }
|
||||
public byte[] Data { get; }
|
||||
public AssemblyDefinition Definition { get; }
|
||||
|
||||
public ReferencedAssembly(AssemblyDefinition definition)
|
||||
{
|
||||
Name = definition.Name.Name;
|
||||
Data = File.ReadAllBytes(definition.MainModule.FileName);
|
||||
Definition = definition;
|
||||
}
|
||||
|
||||
public ReferencedAssembly(IFileInfo fileInfo)
|
||||
{
|
||||
using (var ms = new MemoryStream())
|
||||
using (var readStream = fileInfo.CreateReadStream())
|
||||
{
|
||||
readStream.CopyTo(ms);
|
||||
Data = ms.ToArray();
|
||||
}
|
||||
|
||||
using (var readStream = new MemoryStream(Data))
|
||||
{
|
||||
Definition = AssemblyDefinition.ReadAssembly(readStream);
|
||||
}
|
||||
|
||||
Name = Definition.Name.Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue