Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EnC: Fix document out-of-sync checks when module is not loaded when d… #39836

Merged
merged 5 commits into from
Nov 18, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,16 @@ TService IDocumentServiceProvider.GetService<TService>()
}

private (DebuggeeModuleInfo, Guid) EmitAndLoadLibraryToDebuggee(string source, ProjectId projectId, string assemblyName = "", string sourceFilePath = "test1.cs")
{
var (debuggeeModuleInfo, moduleId) = EmitLibrary(source, projectId, assemblyName, sourceFilePath);
LoadLibraryToDebuggee(debuggeeModuleInfo);
return (debuggeeModuleInfo, moduleId);
}

private void LoadLibraryToDebuggee(DebuggeeModuleInfo debuggeeModuleInfo)
=> _mockDebugeeModuleMetadataProvider.TryGetBaselineModuleInfo = mvid => debuggeeModuleInfo;

private (DebuggeeModuleInfo, Guid) EmitLibrary(string source, ProjectId projectId, string assemblyName = "", string sourceFilePath = "test1.cs")
{
var sourceText = SourceText.From(new MemoryStream(Encoding.UTF8.GetBytes(source)), encoding: Encoding.UTF8, checksumAlgorithm: SourceHashAlgorithm.Sha256);
var tree = SyntaxFactory.ParseSyntaxTree(sourceText, TestOptions.RegularPreview, sourceFilePath);
Expand All @@ -148,9 +158,6 @@ TService IDocumentServiceProvider.GetService<TService>()
var moduleId = moduleMetadata.GetModuleVersionId();
var debuggeeModuleInfo = new DebuggeeModuleInfo(moduleMetadata, symReader);

// "load" it to the debuggee:
_mockDebugeeModuleMetadataProvider.TryGetBaselineModuleInfo = mvid => debuggeeModuleInfo;

// associate the binaries with the project
_mockCompilationOutputsService.Outputs.Add(projectId, new MockCompilationOutputs(moduleId));

Expand All @@ -163,7 +170,6 @@ private SourceText CreateSourceTextFromFile(string path)
return SourceText.From(stream, Encoding.UTF8, SourceHashAlgorithm.Sha256);
}


[Fact]
public void ActiveStatementTracking()
{
Expand Down Expand Up @@ -1021,6 +1027,70 @@ public async Task BreakMode_RudeEdits_DocumentWithoutSequencePoints()
service.EndDebuggingSession();
}

[Fact]
public async Task BreakMode_RudeEdits_DelayLoadedModule()
{
var source1 = "class C { public void M() { } }";
var dir = Temp.CreateDirectory();
var sourceFile = dir.CreateFile("a.cs").WriteAllText(source1);

using var workspace = new TestWorkspace();

// the workspace starts with a version of the source that's not updated with the output of single file generator (or design-time build):
var document1 = workspace.CurrentSolution.
AddProject("test", "test", LanguageNames.CSharp).
AddMetadataReferences(TargetFrameworkUtil.GetReferences(TargetFramework.Mscorlib40)).
AddDocument("test.cs", SourceText.From(source1, Encoding.UTF8), filePath: sourceFile.Path);

var project = document1.Project;
workspace.ChangeSolution(project.Solution);

var (debuggeeModuleInfo, _) = EmitLibrary(source1, project.Id, sourceFilePath: sourceFile.Path);

var service = CreateEditAndContinueService(workspace);

// do not initialize the document state - we will detect the state based on the PDB content.
var debuggingSession = StartDebuggingSession(service, initialState: CommittedSolution.DocumentState.None);

service.StartEditSession();

// change the source (rude edit) before the library is loaded:
workspace.ChangeDocument(document1.Id, SourceText.From("class C { public void Renamed() { } }"));
var document2 = workspace.CurrentSolution.Projects.Single().Documents.Single();

// Rude Edits reported:
var diagnostics = await service.GetDocumentDiagnosticsAsync(document2, CancellationToken.None).ConfigureAwait(false);
AssertEx.Equal(
new[] { "ENC0020: " + string.Format(FeaturesResources.Renaming_0_will_prevent_the_debug_session_from_continuing, FeaturesResources.method) },
diagnostics.Select(d => $"{d.Id}: {d.GetMessage()}"));

var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatus);

var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false);
Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatusEmit);
Assert.Empty(deltas);

// load library to the debuggee:
LoadLibraryToDebuggee(debuggeeModuleInfo);

// Rude Edits still reported:
diagnostics = await service.GetDocumentDiagnosticsAsync(document2, CancellationToken.None).ConfigureAwait(false);
AssertEx.Equal(
new[] { "ENC0020: " + string.Format(FeaturesResources.Renaming_0_will_prevent_the_debug_session_from_continuing, FeaturesResources.method) },
diagnostics.Select(d => $"{d.Id}: {d.GetMessage()}"));

solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatus);

(solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false);
Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatusEmit);
Assert.Empty(deltas);

service.EndEditSession();
service.EndDebuggingSession();
}

[Fact]
public async Task BreakMode_SyntaxError()
{
Expand Down
115 changes: 78 additions & 37 deletions src/Features/Core/Portable/EditAndContinue/CommittedSolution.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,32 @@ internal sealed class CommittedSolution
internal enum DocumentState
{
None = 0,
OutOfSync = 1,
MatchesDebuggee = 2,
DesignTimeOnly = 3,

/// <summary>
/// The document belongs to a project whose compiled module has not been loaded yet.
/// This document state may change to to <see cref="OutOfSync"/>, <see cref="MatchesDebuggee"/>
tmat marked this conversation as resolved.
Show resolved Hide resolved
/// or <see cref="DesignTimeOnly"/> once the module has been loaded.
/// </summary>
ModuleNotLoaded = 1,

/// <summary>
/// The current document content does not match the content the module was compiled with.
/// This document state may change to <see cref="MatchesDebuggee"/> or <see cref="DesignTimeOnly"/>.
/// </summary>
OutOfSync = 2,

/// <summary>
/// The current document content matches the content the module was compiled with.
/// This is a final state. Once a document is in this state it won't switch to a different one.
/// </summary>
MatchesDebuggee = 3,

/// <summary>
/// The document is not compiled into the module. It's only included in the project
/// to support design-time features such as completion, etc.
/// This is a final state. Once a document is in this state it won't switch to a different one.
/// </summary>
DesignTimeOnly = 4,
}

/// <summary>
Expand Down Expand Up @@ -138,6 +161,10 @@ public Task OnSourceFileUpdatedAsync(DocumentId documentId, CancellationToken ca
case DocumentState.DesignTimeOnly:
return (null, documentState);

case DocumentState.ModuleNotLoaded:
// module might have been loaded since the last time we checked
break;

case DocumentState.OutOfSync:
if (reloadOutOfSyncDocument)
{
Expand All @@ -152,11 +179,14 @@ public Task OnSourceFileUpdatedAsync(DocumentId documentId, CancellationToken ca
}
}

var (matchingSourceText, isMissing) = await TryGetPdbMatchingSourceTextAsync(document.FilePath, document.Project.Id, cancellationToken).ConfigureAwait(false);
var (matchingSourceText, isLoaded, isMissing) = await TryGetPdbMatchingSourceTextAsync(document.FilePath, document.Project.Id, cancellationToken).ConfigureAwait(false);

lock (_guard)
{
if (_documentState.TryGetValue(documentId, out var documentState) && documentState != DocumentState.OutOfSync)
// only OutOfSync and ModuleNotLoaded states can be changed:
if (_documentState.TryGetValue(documentId, out var documentState) &&
documentState != DocumentState.OutOfSync &&
documentState != DocumentState.ModuleNotLoaded)
{
return (document, documentState);
}
Expand All @@ -166,11 +196,20 @@ public Task OnSourceFileUpdatedAsync(DocumentId documentId, CancellationToken ca

if (isMissing)
{
// Source file is not listed in the PDB. This may happen for a couple of reasons:
// The library wasn't built with that source file - the file has been added before debugging session started but after build captured it.
// This is the case for WPF .g.i.cs files.
matchingDocument = null;
newState = DocumentState.DesignTimeOnly;
if (isLoaded)
{
// Source file is not listed in the PDB. This may happen for a couple of reasons:
// The library wasn't built with that source file - the file has been added before debugging session started but after build captured it.
// This is the case for WPF .g.i.cs files.
matchingDocument = null;
newState = DocumentState.DesignTimeOnly;
}
else
{
// The module the document is compiled into has not been loaded yet.
matchingDocument = document;
newState = DocumentState.ModuleNotLoaded;
}
}
else if (matchingSourceText != null)
{
Expand Down Expand Up @@ -216,34 +255,34 @@ public void CommitSolution(Solution solution, ImmutableArray<Document> updatedDo
}
}

private async Task<(SourceText? Source, bool IsMissing)> TryGetPdbMatchingSourceTextAsync(string sourceFilePath, ProjectId projectId, CancellationToken cancellationToken)
private async Task<(SourceText? Source, bool IsLoaded, bool IsMissing)> TryGetPdbMatchingSourceTextAsync(string sourceFilePath, ProjectId projectId, CancellationToken cancellationToken)
{
var (symChecksum, algorithm) = await TryReadSourceFileChecksumFromPdb(sourceFilePath, projectId, cancellationToken).ConfigureAwait(false);
var (symChecksum, algorithm, isLoaded) = await TryReadSourceFileChecksumFromPdb(sourceFilePath, projectId, cancellationToken).ConfigureAwait(false);
if (symChecksum.IsDefault)
{
return (Source: null, IsMissing: true);
return (Source: null, isLoaded, IsMissing: true);
}

if (!PathUtilities.IsAbsolute(sourceFilePath))
{
EditAndContinueWorkspaceService.Log.Write("Error calculating checksum for source file '{0}': path not absolute", sourceFilePath);
return (Source: null, IsMissing: false);
return (Source: null, isLoaded, IsMissing: false);
}

try
{
using var fileStream = new FileStream(sourceFilePath, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete);
var sourceText = SourceText.From(fileStream, checksumAlgorithm: algorithm);
return (sourceText.GetChecksum().SequenceEqual(symChecksum) ? sourceText : null, IsMissing: false);
return (sourceText.GetChecksum().SequenceEqual(symChecksum) ? sourceText : null, isLoaded, IsMissing: false);
}
catch (Exception e)
{
EditAndContinueWorkspaceService.Log.Write("Error calculating checksum for source file '{0}': '{1}'", sourceFilePath, e.Message);
return (Source: null, IsMissing: false);
return (Source: null, isLoaded, IsMissing: false);
}
}

private async Task<(ImmutableArray<byte> Checksum, SourceHashAlgorithm Algorithm)> TryReadSourceFileChecksumFromPdb(string sourceFilePath, ProjectId projectId, CancellationToken cancellationToken)
private async Task<(ImmutableArray<byte> Checksum, SourceHashAlgorithm Algorithm, bool IsLoaded)> TryReadSourceFileChecksumFromPdb(string sourceFilePath, ProjectId projectId, CancellationToken cancellationToken)
{
try
{
Expand All @@ -259,58 +298,60 @@ public void CommitSolution(Solution solution, ImmutableArray<Document> updatedDo
// Dispatch to a background thread - reading symbols requires MTA thread.
if (Thread.CurrentThread.GetApartmentState() != ApartmentState.MTA)
{
return await Task.Factory.StartNew(() =>
{
try
{
return ReadChecksum();
}
catch (Exception e) when (FatalError.ReportWithoutCrashUnlessCanceled(e))
{
EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match PDB: unexpected exception: {1}", sourceFilePath, e.Message);
return default;
}
}, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default).ConfigureAwait(false);
return await Task.Factory.StartNew(ReadChecksum, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default).ConfigureAwait(false);
}
else
{
return ReadChecksum();
}

(ImmutableArray<byte> Checksum, SourceHashAlgorithm Algorithm) ReadChecksum()
(ImmutableArray<byte> Checksum, SourceHashAlgorithm Algorithm, bool IsLoaded) ReadChecksum()
{
var moduleInfo = _debuggingSession.DebugeeModuleMetadataProvider.TryGetBaselineModuleInfo(mvid);
DebuggeeModuleInfo? moduleInfo;
bool isLoaded = false;
try
{
moduleInfo = _debuggingSession.DebugeeModuleMetadataProvider.TryGetBaselineModuleInfo(mvid);
}
catch (Exception e) when (FatalError.ReportWithoutCrashUnlessCanceled(e))
{
EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match PDB: unexpected exception: {1}", sourceFilePath, e.Message);
return (default, default, isLoaded);
}

if (moduleInfo == null)
{
EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match PDB: can't get baseline SymReader", sourceFilePath);
return default;
EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match PDB: module not loaded", sourceFilePath);
return (default, default, isLoaded);
}

isLoaded = true;

try
{
var symDocument = moduleInfo.SymReader.GetDocument(sourceFilePath);
if (symDocument == null)
{
EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match PDB: no SymDocument", sourceFilePath);
return default;
return (default, default, isLoaded);
}

var symAlgorithm = SourceHashAlgorithms.GetSourceHashAlgorithm(symDocument.GetHashAlgorithm());
if (symAlgorithm == SourceHashAlgorithm.None)
{
// unknown algorithm:
EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match PDB: unknown checksum alg", sourceFilePath);
return default;
return (default, default, isLoaded);
}

var symChecksum = symDocument.GetChecksum().ToImmutableArray();

return (symChecksum, symAlgorithm);
return (symChecksum, symAlgorithm, isLoaded);
}
catch (Exception e)
{
EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match PDB: error reading symbols: {1}", sourceFilePath, e.Message);
return default;
return (default, default, isLoaded);
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/Features/Core/Portable/EditAndContinue/EditSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,9 @@ internal async Task<ImmutableArray<ActiveStatementExceptionRegions>> GetBaseActi
continue;

default:
// Include the document regardless of whether the module it was built into has been loaded or not.
// If the module has been built it might get loaded later during the debugging session,
// at which point we apply all changes that have been made to the project so far.
changedDocuments.Add((oldDocument, document));
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ internal interface IDebuggeeModuleMetadataProvider
/// Shall only be called while in debug mode.
/// Shall only be called on MTA thread.
/// </summary>
/// <returns>Null, if the module with the specified MVID is not loaded.</returns>
DebuggeeModuleInfo? TryGetBaselineModuleInfo(Guid mvid);

/// <summary>
Expand Down