|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection.Metadata;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Host;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.Serialization
{
internal partial class SerializerService
{
private const int MetadataFailed = int.MaxValue;
private static readonly ConditionalWeakTable<Metadata, object> s_lifetimeMap = new();
public static Checksum CreateChecksum(MetadataReference reference, CancellationToken cancellationToken)
{
if (reference is PortableExecutableReference portable)
{
return CreatePortableExecutableReferenceChecksum(portable, cancellationToken);
}
throw ExceptionUtilities.UnexpectedValue(reference.GetType());
}
private static bool IsAnalyzerReferenceWithShadowCopyLoader(AnalyzerFileReference reference)
=> reference.AssemblyLoader is ShadowCopyAnalyzerAssemblyLoader;
public static Checksum CreateChecksum(AnalyzerReference reference, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
using var stream = SerializableBytes.CreateWritableStream();
using (var writer = new ObjectWriter(stream, leaveOpen: true, cancellationToken))
{
switch (reference)
{
case AnalyzerFileReference file:
writer.WriteString(file.FullPath);
writer.WriteBoolean(IsAnalyzerReferenceWithShadowCopyLoader(file));
break;
default:
throw ExceptionUtilities.UnexpectedValue(reference);
}
}
stream.Position = 0;
return Checksum.Create(stream);
}
public virtual void WriteMetadataReferenceTo(MetadataReference reference, ObjectWriter writer, SolutionReplicationContext context, CancellationToken cancellationToken)
{
if (reference is PortableExecutableReference portable)
{
if (portable is ISupportTemporaryStorage supportTemporaryStorage)
{
if (TryWritePortableExecutableReferenceBackedByTemporaryStorageTo(supportTemporaryStorage, writer, context, cancellationToken))
{
return;
}
}
WritePortableExecutableReferenceTo(portable, writer, cancellationToken);
return;
}
throw ExceptionUtilities.UnexpectedValue(reference.GetType());
}
public virtual MetadataReference ReadMetadataReferenceFrom(ObjectReader reader, CancellationToken cancellationToken)
{
var type = reader.ReadString();
if (type == nameof(PortableExecutableReference))
{
return ReadPortableExecutableReferenceFrom(reader, cancellationToken);
}
throw ExceptionUtilities.UnexpectedValue(type);
}
public virtual void WriteAnalyzerReferenceTo(AnalyzerReference reference, ObjectWriter writer, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
switch (reference)
{
case AnalyzerFileReference file:
writer.WriteString(nameof(AnalyzerFileReference));
writer.WriteString(file.FullPath);
writer.WriteBoolean(IsAnalyzerReferenceWithShadowCopyLoader(file));
break;
default:
throw ExceptionUtilities.UnexpectedValue(reference);
}
}
public virtual AnalyzerReference ReadAnalyzerReferenceFrom(ObjectReader reader, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var type = reader.ReadString();
if (type == nameof(AnalyzerFileReference))
{
var fullPath = reader.ReadString();
var shadowCopy = reader.ReadBoolean();
return new AnalyzerFileReference(fullPath, _analyzerLoaderProvider.GetLoader(new AnalyzerAssemblyLoaderOptions(shadowCopy)));
}
throw ExceptionUtilities.UnexpectedValue(type);
}
protected static void WritePortableExecutableReferenceHeaderTo(
PortableExecutableReference reference, SerializationKinds kind, ObjectWriter writer, CancellationToken cancellationToken)
{
writer.WriteString(nameof(PortableExecutableReference));
writer.WriteInt32((int)kind);
WritePortableExecutableReferencePropertiesTo(reference, writer, cancellationToken);
}
private static void WritePortableExecutableReferencePropertiesTo(PortableExecutableReference reference, ObjectWriter writer, CancellationToken cancellationToken)
{
WriteTo(reference.Properties, writer, cancellationToken);
writer.WriteString(reference.FilePath);
}
private static Checksum CreatePortableExecutableReferenceChecksum(PortableExecutableReference reference, CancellationToken cancellationToken)
{
using var stream = SerializableBytes.CreateWritableStream();
using (var writer = new ObjectWriter(stream, leaveOpen: true, cancellationToken))
{
WritePortableExecutableReferencePropertiesTo(reference, writer, cancellationToken);
WriteMvidsTo(TryGetMetadata(reference), writer, cancellationToken);
}
stream.Position = 0;
return Checksum.Create(stream);
}
private static void WriteMvidsTo(Metadata? metadata, ObjectWriter writer, CancellationToken cancellationToken)
{
if (metadata == null)
{
// handle error case where we couldn't load metadata of the reference.
// this basically won't write anything to writer
return;
}
if (metadata is AssemblyMetadata assemblyMetadata)
{
if (!TryGetModules(assemblyMetadata, out var modules))
{
// Gracefully bail out without writing anything to the writer.
return;
}
writer.WriteInt32((int)assemblyMetadata.Kind);
writer.WriteInt32(modules.Length);
foreach (var module in modules)
{
WriteMvidTo(module, writer, cancellationToken);
}
return;
}
WriteMvidTo((ModuleMetadata)metadata, writer, cancellationToken);
}
private static bool TryGetModules(AssemblyMetadata assemblyMetadata, out ImmutableArray<ModuleMetadata> modules)
{
// Gracefully handle documented exceptions from 'GetModules' invocation.
try
{
modules = assemblyMetadata.GetModules();
return true;
}
catch (Exception ex) when (ex is BadImageFormatException or
IOException or
ObjectDisposedException)
{
modules = default;
return false;
}
}
private static void WriteMvidTo(ModuleMetadata metadata, ObjectWriter writer, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
writer.WriteInt32((int)metadata.Kind);
var metadataReader = metadata.GetMetadataReader();
var mvidHandle = metadataReader.GetModuleDefinition().Mvid;
var guid = metadataReader.GetGuid(mvidHandle);
writer.WriteGuid(guid);
}
private static void WritePortableExecutableReferenceTo(
PortableExecutableReference reference, ObjectWriter writer, CancellationToken cancellationToken)
{
WritePortableExecutableReferenceHeaderTo(reference, SerializationKinds.Bits, writer, cancellationToken);
WriteTo(TryGetMetadata(reference), writer, cancellationToken);
// TODO: what I should do with documentation provider? it is not exposed outside
}
private PortableExecutableReference ReadPortableExecutableReferenceFrom(ObjectReader reader, CancellationToken cancellationToken)
{
var kind = (SerializationKinds)reader.ReadInt32();
if (kind is SerializationKinds.Bits or SerializationKinds.MemoryMapFile)
{
var properties = ReadMetadataReferencePropertiesFrom(reader, cancellationToken);
var filePath = reader.ReadString();
var tuple = TryReadMetadataFrom(reader, kind, cancellationToken);
if (tuple == null)
{
// TODO: deal with xml document provider properly
// should we shadow copy xml doc comment?
// image doesn't exist
return new MissingMetadataReference(properties, filePath, XmlDocumentationProvider.Default);
}
// for now, we will use IDocumentationProviderService to get DocumentationProvider for metadata
// references. if the service is not available, then use Default (NoOp) provider.
// since xml doc comment is not part of solution snapshot, (like xml reference resolver or strong name
// provider) this provider can also potentially provide content that is different than one in the host.
// an alternative approach of this is synching content of xml doc comment to remote host as well
// so that we can put xml doc comment as part of snapshot. but until we believe that is necessary,
// it will go with simpler approach
var documentProvider = filePath != null && _documentationService != null ?
_documentationService.GetDocumentationProvider(filePath) : XmlDocumentationProvider.Default;
return new SerializedMetadataReference(
properties, filePath, tuple.Value.metadata, tuple.Value.storages, documentProvider);
}
throw ExceptionUtilities.UnexpectedValue(kind);
}
private static void WriteTo(MetadataReferenceProperties properties, ObjectWriter writer, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
writer.WriteInt32((int)properties.Kind);
writer.WriteValue(properties.Aliases.ToArray());
writer.WriteBoolean(properties.EmbedInteropTypes);
}
private static MetadataReferenceProperties ReadMetadataReferencePropertiesFrom(ObjectReader reader, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var kind = (MetadataImageKind)reader.ReadInt32();
var aliases = reader.ReadArray<string>().ToImmutableArrayOrEmpty();
var embedInteropTypes = reader.ReadBoolean();
return new MetadataReferenceProperties(kind, aliases, embedInteropTypes);
}
private static void WriteTo(Metadata? metadata, ObjectWriter writer, CancellationToken cancellationToken)
{
if (metadata == null)
{
// handle error case where metadata failed to load
writer.WriteInt32(MetadataFailed);
return;
}
if (metadata is AssemblyMetadata assemblyMetadata)
{
if (!TryGetModules(assemblyMetadata, out var modules))
{
// Gracefully handle error case where unable to get modules.
writer.WriteInt32(MetadataFailed);
return;
}
writer.WriteInt32((int)assemblyMetadata.Kind);
writer.WriteInt32(modules.Length);
foreach (var module in modules)
{
WriteTo(module, writer, cancellationToken);
}
return;
}
WriteTo((ModuleMetadata)metadata, writer, cancellationToken);
}
private static bool TryWritePortableExecutableReferenceBackedByTemporaryStorageTo(
ISupportTemporaryStorage reference, ObjectWriter writer, SolutionReplicationContext context, CancellationToken cancellationToken)
{
var storages = reference.GetStorages();
if (storages == null)
{
return false;
}
// Not clear if name should be allowed to be null here (https://github.com/dotnet/roslyn/issues/43037)
using var pooled = Creator.CreateList<(string? name, long offset, long size)>();
foreach (var storage in storages)
{
if (storage is not ITemporaryStorageWithName storage2)
{
return false;
}
context.AddResource(storage);
pooled.Object.Add((storage2.Name, storage2.Offset, storage2.Size));
}
WritePortableExecutableReferenceHeaderTo((PortableExecutableReference)reference, SerializationKinds.MemoryMapFile, writer, cancellationToken);
writer.WriteInt32((int)MetadataImageKind.Assembly);
writer.WriteInt32(pooled.Object.Count);
foreach (var (name, offset, size) in pooled.Object)
{
writer.WriteInt32((int)MetadataImageKind.Module);
writer.WriteString(name);
writer.WriteInt64(offset);
writer.WriteInt64(size);
}
return true;
}
private (Metadata metadata, ImmutableArray<ITemporaryStreamStorageInternal> storages)? TryReadMetadataFrom(
ObjectReader reader, SerializationKinds kind, CancellationToken cancellationToken)
{
var imageKind = reader.ReadInt32();
if (imageKind == MetadataFailed)
{
// error case
return null;
}
var metadataKind = (MetadataImageKind)imageKind;
if (_storageService == null)
{
if (metadataKind == MetadataImageKind.Assembly)
{
using var pooledMetadata = Creator.CreateList<ModuleMetadata>();
var count = reader.ReadInt32();
for (var i = 0; i < count; i++)
{
metadataKind = (MetadataImageKind)reader.ReadInt32();
Contract.ThrowIfFalse(metadataKind == MetadataImageKind.Module);
#pragma warning disable CA2016 // https://github.com/dotnet/roslyn-analyzers/issues/4985
pooledMetadata.Object.Add(ReadModuleMetadataFrom(reader, kind));
#pragma warning restore CA2016
}
return (AssemblyMetadata.Create(pooledMetadata.Object), storages: default);
}
Contract.ThrowIfFalse(metadataKind == MetadataImageKind.Module);
#pragma warning disable CA2016 // https://github.com/dotnet/roslyn-analyzers/issues/4985
return (ReadModuleMetadataFrom(reader, kind), storages: default);
#pragma warning restore CA2016
}
if (metadataKind == MetadataImageKind.Assembly)
{
using var pooledMetadata = Creator.CreateList<ModuleMetadata>();
using var pooledStorage = Creator.CreateList<ITemporaryStreamStorageInternal>();
var count = reader.ReadInt32();
for (var i = 0; i < count; i++)
{
metadataKind = (MetadataImageKind)reader.ReadInt32();
Contract.ThrowIfFalse(metadataKind == MetadataImageKind.Module);
var (metadata, storage) = ReadModuleMetadataFrom(reader, kind, cancellationToken);
pooledMetadata.Object.Add(metadata);
pooledStorage.Object.Add(storage);
}
return (AssemblyMetadata.Create(pooledMetadata.Object), pooledStorage.Object.ToImmutableArrayOrEmpty());
}
Contract.ThrowIfFalse(metadataKind == MetadataImageKind.Module);
var moduleInfo = ReadModuleMetadataFrom(reader, kind, cancellationToken);
return (moduleInfo.metadata, ImmutableArray.Create(moduleInfo.storage));
}
private (ModuleMetadata metadata, ITemporaryStreamStorageInternal storage) ReadModuleMetadataFrom(
ObjectReader reader, SerializationKinds kind, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
GetTemporaryStorage(reader, kind, out var storage, out var length, cancellationToken);
var storageStream = storage.ReadStream(cancellationToken);
Contract.ThrowIfFalse(length == storageStream.Length);
GetMetadata(storageStream, length, out var metadata, out var lifeTimeObject);
// make sure we keep storageStream alive while Metadata is alive
// we use conditional weak table since we can't control metadata liftetime
if (lifeTimeObject != null)
s_lifetimeMap.Add(metadata, lifeTimeObject);
return (metadata, storage);
}
private static ModuleMetadata ReadModuleMetadataFrom(ObjectReader reader, SerializationKinds kind)
{
Contract.ThrowIfFalse(SerializationKinds.Bits == kind);
var array = reader.ReadArray<byte>();
var pinnedObject = new PinnedObject(array);
var metadata = ModuleMetadata.CreateFromMetadata(pinnedObject.GetPointer(), array.Length);
// make sure we keep storageStream alive while Metadata is alive
// we use conditional weak table since we can't control metadata liftetime
s_lifetimeMap.Add(metadata, pinnedObject);
return metadata;
}
private void GetTemporaryStorage(
ObjectReader reader, SerializationKinds kind, out ITemporaryStreamStorageInternal storage, out long length, CancellationToken cancellationToken)
{
if (kind == SerializationKinds.Bits)
{
storage = _storageService.CreateTemporaryStreamStorage();
using var stream = SerializableBytes.CreateWritableStream();
CopyByteArrayToStream(reader, stream, cancellationToken);
length = stream.Length;
stream.Position = 0;
storage.WriteStream(stream, cancellationToken);
return;
}
if (kind == SerializationKinds.MemoryMapFile)
{
var service2 = (ITemporaryStorageService2)_storageService;
var name = reader.ReadString();
var offset = reader.ReadInt64();
var size = reader.ReadInt64();
storage = service2.AttachTemporaryStreamStorage(name, offset, size);
length = size;
return;
}
throw ExceptionUtilities.UnexpectedValue(kind);
}
private static void GetMetadata(Stream stream, long length, out ModuleMetadata metadata, out object? lifeTimeObject)
{
if (stream is UnmanagedMemoryStream unmanagedStream)
{
// For an unmanaged memory stream, ModuleMetadata can take ownership directly.
unsafe
{
metadata = ModuleMetadata.CreateFromMetadata(
(IntPtr)unmanagedStream.PositionPointer, (int)unmanagedStream.Length, unmanagedStream.Dispose);
lifeTimeObject = null;
return;
}
}
PinnedObject pinnedObject;
if (stream is MemoryStream memory &&
memory.TryGetBuffer(out var buffer) &&
buffer.Offset == 0)
{
pinnedObject = new PinnedObject(buffer.Array!);
}
else
{
var array = new byte[length];
stream.Read(array, 0, (int)length);
pinnedObject = new PinnedObject(array);
}
metadata = ModuleMetadata.CreateFromMetadata(pinnedObject.GetPointer(), (int)length);
lifeTimeObject = pinnedObject;
}
private static void CopyByteArrayToStream(ObjectReader reader, Stream stream, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
// TODO: make reader be able to read byte[] chunk
var content = reader.ReadArray<byte>();
stream.Write(content, 0, content.Length);
}
private static void WriteTo(ModuleMetadata metadata, ObjectWriter writer, CancellationToken cancellationToken)
{
writer.WriteInt32((int)metadata.Kind);
WriteTo(metadata.GetMetadataReader(), writer, cancellationToken);
}
private static unsafe void WriteTo(MetadataReader reader, ObjectWriter writer, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
writer.WriteValue(new ReadOnlySpan<byte>(reader.MetadataPointer, reader.MetadataLength));
}
private static void WriteUnresolvedAnalyzerReferenceTo(AnalyzerReference reference, ObjectWriter writer)
{
writer.WriteString(nameof(UnresolvedAnalyzerReference));
writer.WriteString(reference.FullPath);
}
private static Metadata? TryGetMetadata(PortableExecutableReference reference)
{
try
{
return reference.GetMetadata();
}
catch
{
// we have a reference but the file the reference is pointing to
// might not actually exist on disk.
// in that case, rather than crashing, we will handle it gracefully.
return null;
}
}
private sealed class PinnedObject : IDisposable
{
// shouldn't be read-only since GCHandle is a mutable struct
private GCHandle _gcHandle;
public PinnedObject(byte[] array)
=> _gcHandle = GCHandle.Alloc(array, GCHandleType.Pinned);
internal IntPtr GetPointer()
=> _gcHandle.AddrOfPinnedObject();
private void OnDispose()
{
if (_gcHandle.IsAllocated)
{
_gcHandle.Free();
}
}
~PinnedObject()
=> OnDispose();
public void Dispose()
{
GC.SuppressFinalize(this);
OnDispose();
}
}
private sealed class MissingMetadataReference : PortableExecutableReference
{
private readonly DocumentationProvider _provider;
public MissingMetadataReference(
MetadataReferenceProperties properties, string? fullPath, DocumentationProvider initialDocumentation)
: base(properties, fullPath, initialDocumentation)
{
// TODO: doc comment provider is a bit weird.
_provider = initialDocumentation;
}
protected override DocumentationProvider CreateDocumentationProvider()
{
// TODO: properly implement this
throw new NotImplementedException();
}
protected override Metadata GetMetadataImpl()
{
// we just throw "FileNotFoundException" even if it might not be actual reason
// why metadata has failed to load. in this context, we don't care much on actual
// reason. we just need to maintain failure when re-constructing solution to maintain
// snapshot integrity.
//
// if anyone care actual reason, he should get that info from original Solution.
throw new FileNotFoundException(FilePath);
}
protected override PortableExecutableReference WithPropertiesImpl(MetadataReferenceProperties properties)
=> new MissingMetadataReference(properties, FilePath, _provider);
}
[DebuggerDisplay("{" + nameof(Display) + ",nq}")]
private sealed class SerializedMetadataReference : PortableExecutableReference, ISupportTemporaryStorage
{
private readonly Metadata _metadata;
private readonly ImmutableArray<ITemporaryStreamStorageInternal> _storagesOpt;
private readonly DocumentationProvider _provider;
public SerializedMetadataReference(
MetadataReferenceProperties properties, string? fullPath,
Metadata metadata, ImmutableArray<ITemporaryStreamStorageInternal> storagesOpt, DocumentationProvider initialDocumentation)
: base(properties, fullPath, initialDocumentation)
{
_metadata = metadata;
_storagesOpt = storagesOpt;
_provider = initialDocumentation;
}
protected override DocumentationProvider CreateDocumentationProvider()
{
// this uses documentation provider given at the constructor
throw ExceptionUtilities.Unreachable();
}
protected override Metadata GetMetadataImpl()
=> _metadata;
protected override PortableExecutableReference WithPropertiesImpl(MetadataReferenceProperties properties)
=> new SerializedMetadataReference(properties, FilePath, _metadata, _storagesOpt, _provider);
public IEnumerable<ITemporaryStreamStorageInternal>? GetStorages()
=> _storagesOpt.IsDefault ? null : _storagesOpt;
}
}
}
|