From 81721920c97b5eafae1b7a45313ff752a0b3ba03 Mon Sep 17 00:00:00 2001 From: Heejae Chang Date: Fri, 23 Sep 2016 03:38:20 -0700 Subject: [PATCH] Merge pull request #13997 from heejaechang/handleException MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit made serialization service to resilliant to IO exceptions such as mis… --- .../AbstractReferenceSerializationService.cs | 142 +++++++++++++++--- .../Execution/SnapshotSerializationTests.cs | 131 +++++++++++++++- 2 files changed, 249 insertions(+), 24 deletions(-) diff --git a/src/Workspaces/Core/Portable/Execution/AbstractReferenceSerializationService.cs b/src/Workspaces/Core/Portable/Execution/AbstractReferenceSerializationService.cs index 59f896d1aed10..5ac081bfd6cbb 100644 --- a/src/Workspaces/Core/Portable/Execution/AbstractReferenceSerializationService.cs +++ b/src/Workspaces/Core/Portable/Execution/AbstractReferenceSerializationService.cs @@ -18,6 +18,8 @@ namespace Microsoft.CodeAnalysis.Execution { internal abstract class AbstractReferenceSerializationService : IReferenceSerializationService { + private const int MetadataFailed = int.MaxValue; + private static readonly ConditionalWeakTable s_lifetimeMap = new ConditionalWeakTable(); private readonly ITemporaryStorageService _storageService; @@ -92,6 +94,14 @@ public void WriteTo(AnalyzerReference reference, ObjectWriter writer, Cancellati var file = reference as AnalyzerFileReference; if (file != null) { + // fail to load analyzer assembly + var assemblyPath = TryGetAnalyzerAssemblyPath(file); + if (assemblyPath == null) + { + WriteUnresolvedAnalyzerReferenceTo(reference, writer); + return; + } + writer.WriteString(nameof(AnalyzerFileReference)); writer.WriteInt32((int)SerializationKinds.FilePath); @@ -104,7 +114,14 @@ public void WriteTo(AnalyzerReference reference, ObjectWriter writer, Cancellati // snapshot version for analyzer (since it is based on shadow copy) // we can't send over bits and load analyer from memory (image) due to CLR not being able // to find satellite dlls for analyzers. - writer.WriteString(GetAnalyzerAssemblyPath(file)); + writer.WriteString(assemblyPath); + return; + } + + var unresolved = reference as UnresolvedAnalyzerReference; + if (unresolved != null) + { + WriteUnresolvedAnalyzerReferenceTo(reference, writer); return; } @@ -115,14 +132,6 @@ public void WriteTo(AnalyzerReference reference, ObjectWriter writer, Cancellati throw new NotSupportedException(nameof(AnalyzerImageReference)); } - var unresolved = reference as UnresolvedAnalyzerReference; - if (unresolved != null) - { - writer.WriteString(nameof(UnresolvedAnalyzerReference)); - writer.WriteString(reference.FullPath); - return; - } - throw ExceptionUtilities.UnexpectedValue(reference.GetType()); } @@ -167,16 +176,21 @@ private Checksum CreatePortableExecutableReferenceChecksum(PortableExecutableRef using (var stream = SerializableBytes.CreateWritableStream()) using (var writer = new ObjectWriter(stream, cancellationToken: cancellationToken)) { - WriteMvidsTo(reference, writer, cancellationToken); + WriteMvidsTo(TryGetMetadata(reference), writer, cancellationToken); stream.Position = 0; return Checksum.Create(stream); } } - private void WriteMvidsTo(PortableExecutableReference reference, ObjectWriter writer, CancellationToken cancellationToken) + private void WriteMvidsTo(Metadata metadata, ObjectWriter writer, CancellationToken cancellationToken) { - var metadata = reference.GetMetadata(); + if (metadata == null) + { + // handle error case where we couldn't load metadata of the reference. + // this basically won't write anything to writer + return; + } var assemblyMetadata = metadata as AssemblyMetadata; if (assemblyMetadata != null) @@ -216,7 +230,7 @@ private void WritePortableExecutableReferenceTo( { WritePortableExecutableReferenceHeaderTo(reference, SerializationKinds.Bits, writer, cancellationToken); - WriteTo(reference.GetMetadata(), writer, cancellationToken); + WriteTo(TryGetMetadata(reference), writer, cancellationToken); // TODO: what I should do with documentation provider? it is not exposed outside } @@ -230,11 +244,19 @@ private PortableExecutableReference ReadPortableExecutableReferenceFrom(ObjectRe var filePath = reader.ReadString(); - var tuple = ReadMetadataFrom(reader, kind, cancellationToken); + 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); + } // TODO: deal with xml document provider properly - // should be shadow copy xml doc comment? - return new SerializedMetadataReference(properties, filePath, tuple.Item1, tuple.Item2, XmlDocumentationProvider.Default); + // should we shadow copy xml doc comment? + return new SerializedMetadataReference(properties, filePath, tuple.Value.Item1, tuple.Value.Item2, XmlDocumentationProvider.Default); } throw ExceptionUtilities.UnexpectedValue(kind); @@ -262,6 +284,13 @@ private MetadataReferenceProperties ReadMetadataReferencePropertiesFrom(ObjectRe private void WriteTo(Metadata metadata, ObjectWriter writer, CancellationToken cancellationToken) { + if (metadata == null) + { + // handle error case where metadata failed to load + writer.WriteInt32(MetadataFailed); + return; + } + var assemblyMetadata = metadata as AssemblyMetadata; if (assemblyMetadata != null) { @@ -319,10 +348,17 @@ private bool TryWritePortableExecutableReferenceBackedByTemporaryStorageTo( } } - private ValueTuple> ReadMetadataFrom( + private ValueTuple>? TryReadMetadataFrom( ObjectReader reader, SerializationKinds kind, CancellationToken cancellationToken) { - var metadataKind = (MetadataImageKind)reader.ReadInt32(); + var imageKind = reader.ReadInt32(); + if (imageKind == MetadataFailed) + { + // error case + return null; + } + + var metadataKind = (MetadataImageKind)imageKind; if (metadataKind == MetadataImageKind.Assembly) { using (var pooledMetadata = Creator.CreateList()) @@ -466,6 +502,41 @@ private unsafe void WriteTo(MetadataReader reader, ObjectWriter writer, Cancella writer.WriteValue(bytes); } + 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 string TryGetAnalyzerAssemblyPath(AnalyzerFileReference file) + { + try + { + return GetAnalyzerAssemblyPath(file); + } + catch + { + // we can't load the assembly analyzer file reference is pointing to. + // rather than crashing, handle it gracefully + return null; + } + } + private sealed class PinnedObject : IDisposable { private readonly GCHandle _gcHandle; @@ -500,6 +571,41 @@ public void Dispose() } } + 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 wierd. + _provider = initialDocumentation; + } + + protected override DocumentationProvider CreateDocumentationProvider() + { + // TODO: properly implement this + return null; + } + + 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) + { + return new MissingMetadataReference(properties, FilePath, _provider); + } + } + private sealed class SerializedMetadataReference : PortableExecutableReference, ISupportTemporaryStorage { private readonly Metadata _metadata; diff --git a/src/Workspaces/CoreTest/Execution/SnapshotSerializationTests.cs b/src/Workspaces/CoreTest/Execution/SnapshotSerializationTests.cs index d9537cdba7895..d24eae4b4b587 100644 --- a/src/Workspaces/CoreTest/Execution/SnapshotSerializationTests.cs +++ b/src/Workspaces/CoreTest/Execution/SnapshotSerializationTests.cs @@ -2,7 +2,9 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeStyle; @@ -315,8 +317,8 @@ public async Task MetadataReference_RoundTrip_Test() var assetBuilder = new AssetBuilder(trees.CreateRootTreeNode(workspace.CurrentSolution.State)); var assetFromFile = assetBuilder.Build(reference, CancellationToken.None); - var assetFromStorage = await CloneMetadataReferenceAssetAsync(serializer, assetBuilder, assetFromFile).ConfigureAwait(false); - var assetFromStorage2 = await CloneMetadataReferenceAssetAsync(serializer, assetBuilder, assetFromStorage).ConfigureAwait(false); + var assetFromStorage = await CloneAssetAsync(serializer, assetBuilder, assetFromFile).ConfigureAwait(false); + var assetFromStorage2 = await CloneAssetAsync(serializer, assetBuilder, assetFromStorage).ConfigureAwait(false); } [Fact] @@ -417,6 +419,80 @@ public async Task OptionSet_Serialization_CustomValue() await VerifyOptionSetsAsync(workspace, LanguageNames.VisualBasic).ConfigureAwait(false); } + [Fact] + public async Task Missing_Metadata_Serailization_Test() + { + var workspace = new AdhocWorkspace(); + var reference = new MissingMetadataReference(); + + var serializer = new Serializer(workspace.Services); + var trees = new ChecksumTreeCollection(); + var assetBuilder = new AssetBuilder(trees.CreateRootTreeNode(workspace.CurrentSolution.State)); + + // make sure this doesn't throw + var assetFromFile = assetBuilder.Build(reference, CancellationToken.None); + var assetFromStorage = await CloneAssetAsync(serializer, assetBuilder, assetFromFile).ConfigureAwait(false); + var assetFromStorage2 = await CloneAssetAsync(serializer, assetBuilder, assetFromStorage).ConfigureAwait(false); + } + + [Fact] + public async Task Missing_Analyzer_Serailization_Test() + { + var workspace = new AdhocWorkspace(); + var reference = new AnalyzerFileReference("missing_reference", new MissingAnalyzerLoader()); + + var serializer = new Serializer(workspace.Services); + var trees = new ChecksumTreeCollection(); + var assetBuilder = new AssetBuilder(trees.CreateRootTreeNode(workspace.CurrentSolution.State)); + + // make sure this doesn't throw + var assetFromFile = assetBuilder.Build(reference, CancellationToken.None); + var assetFromStorage = await CloneAssetAsync(serializer, assetBuilder, assetFromFile).ConfigureAwait(false); + var assetFromStorage2 = await CloneAssetAsync(serializer, assetBuilder, assetFromStorage).ConfigureAwait(false); + } + + [Fact] + public async Task Missing_Analyzer_Serailization_Desktop_Test() + { + var hostServices = MefHostServices.Create( + MefHostServices.DefaultAssemblies.Add(typeof(Host.TemporaryStorageServiceFactory.TemporaryStorageService).Assembly)); + + var workspace = new AdhocWorkspace(hostServices); + var reference = new AnalyzerFileReference("missing_reference", new MissingAnalyzerLoader()); + + var serializer = new Serializer(workspace.Services); + var trees = new ChecksumTreeCollection(); + var assetBuilder = new AssetBuilder(trees.CreateRootTreeNode(workspace.CurrentSolution.State)); + + // make sure this doesn't throw + var assetFromFile = assetBuilder.Build(reference, CancellationToken.None); + var assetFromStorage = await CloneAssetAsync(serializer, assetBuilder, assetFromFile).ConfigureAwait(false); + var assetFromStorage2 = await CloneAssetAsync(serializer, assetBuilder, assetFromStorage).ConfigureAwait(false); + } + + [Fact] + public async Task SnapshotWithMissingReferencesTest() + { + var hostServices = MefHostServices.Create( + MefHostServices.DefaultAssemblies.Add(typeof(Host.TemporaryStorageServiceFactory.TemporaryStorageService).Assembly)); + + var solution = new AdhocWorkspace(hostServices).CurrentSolution; + var project1 = solution.AddProject("Project", "Project.dll", LanguageNames.CSharp); + + var metadata = new MissingMetadataReference(); + var analyzer = new AnalyzerFileReference("missing_reference", new MissingAnalyzerLoader()); + + project1 = project1.AddMetadataReference(metadata); + project1 = project1.AddAnalyzerReference(analyzer); + + var snapshotService = (new SolutionChecksumServiceFactory()).CreateService(solution.Workspace.Services) as ISolutionChecksumService; + using (var snapshot = await snapshotService.CreateChecksumAsync(project1.Solution, CancellationToken.None).ConfigureAwait(false)) + { + // this shouldn't throw + var recovered = await GetSolutionAsync(snapshotService, snapshot).ConfigureAwait(false); + } + } + private static async Task VerifyOptionSetsAsync(Workspace workspace, string language) { var assetBuilder = new AssetBuilder(workspace.CurrentSolution); @@ -523,7 +599,7 @@ private async Task GetSolutionAsync(ISolutionChecksumService service, return workspace.AddSolution(SolutionInfo.Create(solutionInfo.Id, solutionInfo.Version, solutionInfo.FilePath, projects)); } - private static async Task CloneMetadataReferenceAssetAsync(Serializer serializer, AssetBuilder assetBuilder, Asset asset) + private static async Task CloneAssetAsync(Serializer serializer, AssetBuilder assetBuilder, Asset asset) { using (var stream = SerializableBytes.CreateWritableStream()) using (var writer = new ObjectWriter(stream)) @@ -533,14 +609,57 @@ private static async Task CloneMetadataReferenceAssetAsync(Serializer ser stream.Position = 0; using (var reader = new ObjectReader(stream)) { - var recovered = serializer.Deserialize(asset.Kind, reader, CancellationToken.None); - var assetFromStorage = assetBuilder.Build(recovered, CancellationToken.None); + var recovered = serializer.Deserialize(asset.Kind, reader, CancellationToken.None); + var assetFromStorage = BuildAsset(assetBuilder, asset.Kind, recovered); Assert.Equal(asset.Checksum, assetFromStorage.Checksum); - return assetFromStorage; } } } + + private static Asset BuildAsset(AssetBuilder builder, string kind, object value) + { + switch (kind) + { + case WellKnownChecksumObjects.AnalyzerReference: + return builder.Build((AnalyzerReference)value, CancellationToken.None); + case WellKnownChecksumObjects.MetadataReference: + return builder.Build((MetadataReference)value, CancellationToken.None); + default: + throw ExceptionUtilities.UnexpectedValue(kind); + } + } + + private class MissingAnalyzerLoader : AnalyzerAssemblyLoader + { + protected override Assembly LoadFromPathImpl(string fullPath) + { + throw new FileNotFoundException(fullPath); + } + } + + private class MissingMetadataReference : PortableExecutableReference + { + public MissingMetadataReference() : + base(MetadataReferenceProperties.Assembly, "missing_reference", XmlDocumentationProvider.Default) + { + } + + protected override DocumentationProvider CreateDocumentationProvider() + { + return null; + } + + protected override Metadata GetMetadataImpl() + { + throw new FileNotFoundException("can't find"); + } + + protected override PortableExecutableReference WithPropertiesImpl(MetadataReferenceProperties properties) + { + return this; + } + } } } \ No newline at end of file