From b06d2ce0737f7ab545be671048cfa689e00bc488 Mon Sep 17 00:00:00 2001 From: Kirill Osenkov Date: Thu, 2 Aug 2018 16:41:57 -0700 Subject: [PATCH] Add APIs to read logs from a Stream, not just file path. --- .../BinaryLoggerTests.cs | 73 +++++ .../BinarySerializationTests.cs | 20 +- src/StructuredLogger.Tests/Differ.cs | 2 +- src/StructuredLogger.Tests/MSBuild.cs | 58 ++++ src/StructuredLogger.Tests/Roundtrip.cs | 3 +- .../StructuredLogger.Tests.csproj | 2 +- src/StructuredLogger/BinaryLog.cs | 29 +- .../BinaryLogReplayEventSource.cs | 251 ++++++++++-------- .../BinaryLogger/ProjectImportsCollector.cs | 2 +- .../Serialization/Binary/BinaryLogReader.cs | 33 ++- .../Serialization/Binary/TreeBinaryReader.cs | 28 +- .../Serialization/Serialization.cs | 4 + .../Serialization/XmlLogReader.cs | 63 +++-- 13 files changed, 410 insertions(+), 158 deletions(-) create mode 100644 src/StructuredLogger.Tests/BinaryLoggerTests.cs create mode 100644 src/StructuredLogger.Tests/MSBuild.cs diff --git a/src/StructuredLogger.Tests/BinaryLoggerTests.cs b/src/StructuredLogger.Tests/BinaryLoggerTests.cs new file mode 100644 index 00000000..ca94ba74 --- /dev/null +++ b/src/StructuredLogger.Tests/BinaryLoggerTests.cs @@ -0,0 +1,73 @@ +using System; +using Microsoft.Build.Logging; +using Microsoft.Build.Logging.StructuredLogger; +using StructuredLogger.Tests; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Build.UnitTests +{ + public class BinaryLoggerTests : IDisposable + { + private static string s_testProject = @" + + + Test + + + + + + + + + + + "; + + public BinaryLoggerTests(ITestOutputHelper output) + { + } + + [Fact] + public void TestBinaryLoggerRoundtrip() + { + var binLog = "1.binlog"; + var binaryLogger = new BinaryLogger(); + binaryLogger.Parameters = binLog; + MSBuild.BuildProject(s_testProject, binaryLogger); + + var build = Serialization.Read(binLog); + Assert.Equal("", GetProperty(build)); + Serialization.Write(build, "1.xml"); + + Serialization.Write(build, "1.buildlog"); + build = Serialization.Read("1.buildlog"); + Assert.Equal("", GetProperty(build)); + Serialization.Write(build, "2.xml"); + + Assert.False(Differ.AreDifferent("1.xml", "2.xml")); + + build = XlinqLogReader.ReadFromXml("1.xml"); + Assert.Equal("", GetProperty(build)); + Serialization.Write(build, "3.xml"); + Assert.False(Differ.AreDifferent("1.xml", "3.xml")); + + build = Serialization.Read("1.xml"); + Assert.Equal("", GetProperty(build)); + Serialization.Write(build, "4.xml"); + + Assert.False(Differ.AreDifferent("1.xml", "4.xml")); + } + + private static string GetProperty(Logging.StructuredLogger.Build build) + { + var property = build.FindFirstDescendant().FindChild("Properties").FindChild(p => p.Name == "FrameworkSDKRoot").Value; + return property; + } + + public void Dispose() + { + } + } +} \ No newline at end of file diff --git a/src/StructuredLogger.Tests/BinarySerializationTests.cs b/src/StructuredLogger.Tests/BinarySerializationTests.cs index 1f96f1bb..cae5fd1e 100644 --- a/src/StructuredLogger.Tests/BinarySerializationTests.cs +++ b/src/StructuredLogger.Tests/BinarySerializationTests.cs @@ -12,22 +12,22 @@ public void TestWriter() Serialization.Write(build, @"D:\1.buildlog"); } - //[Fact] + [Fact] public void SimpleBuild() { var build = new Build(); build.Succeeded = true; build.AddChild(new Message() { Text = "MessageText" }); build.AddChild(new Property() { Name = "PropertyName", Value = "PropertyValue" }); - var file1 = @"D:\1.xml"; - var file2 = @"D:\2.xml"; - Serialization.Write(build, file1); - var filePath = @"D:\1.buildlog"; - Serialization.Write(build, filePath); - build = Serialization.Read(filePath); - Serialization.Write(build, file2); - Serialization.Write(build, @"D:\2.buildlog"); - Differ.AreDifferent(file1, file2); + var xmlFile1 = @"1.xml"; + var xmlFile2 = @"2.xml"; + Serialization.Write(build, xmlFile1); + var buildLogFile = @"1.buildlog"; + Serialization.Write(build, buildLogFile); + build = Serialization.Read(buildLogFile); + Serialization.Write(build, xmlFile2); + Serialization.Write(build, @"2.buildlog"); + Differ.AreDifferent(xmlFile1, xmlFile2); } } } diff --git a/src/StructuredLogger.Tests/Differ.cs b/src/StructuredLogger.Tests/Differ.cs index 3b901f0c..51b139c4 100644 --- a/src/StructuredLogger.Tests/Differ.cs +++ b/src/StructuredLogger.Tests/Differ.cs @@ -11,7 +11,7 @@ public static bool AreDifferent(string file1, string file2) var destination = File.ReadAllText(file2); if (source != destination) { - Process.Start("devenv", $"/diff \"{file1}\" \"{file2}\""); + // Process.Start("devenv", $"/diff \"{Path.GetFullPath(file1)}\" \"{Path.GetFullPath(file2)}\""); return true; } else diff --git a/src/StructuredLogger.Tests/MSBuild.cs b/src/StructuredLogger.Tests/MSBuild.cs new file mode 100644 index 00000000..32c20f12 --- /dev/null +++ b/src/StructuredLogger.Tests/MSBuild.cs @@ -0,0 +1,58 @@ +using System; +using System.IO; +using System.Xml; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Framework; + +namespace StructuredLogger.Tests +{ + public class MSBuild + { + public static bool BuildProject(string projectText, params ILogger[] loggers) + { + Project project = CreateInMemoryProject(projectText, loggers: loggers); + bool success = project.Build(loggers); + return success; + } + + public static Project CreateInMemoryProject(string projectText, ProjectCollection projectCollection = null, params ILogger[] loggers) + { + XmlReaderSettings readerSettings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore }; + projectCollection = projectCollection ?? new ProjectCollection(); + + Project project = new Project( + XmlReader.Create(new StringReader(CleanupFileContents(projectText)), readerSettings), + null, + toolsVersion: null, + projectCollection: projectCollection); + + Guid guid = Guid.NewGuid(); + project.FullPath = Path.GetFullPath("Temporary" + guid.ToString("N") + ".csproj"); + project.ReevaluateIfNecessary(); + + return project; + } + + const string msbuildNamespace = "http://schemas.microsoft.com/developer/msbuild/2003"; + + /// + /// Does certain replacements in a string representing the project file contents. + /// This makes it easier to write unit tests because the author doesn't have + /// to worry about escaping double-quotes, etc. + /// + /// + /// + internal static string CleanupFileContents(string projectFileContents) + { + // Replace reverse-single-quotes with double-quotes. + projectFileContents = projectFileContents.Replace("`", "\""); + + // Place the correct MSBuild namespace into the tag. + projectFileContents = projectFileContents.Replace("msbuildnamespace", msbuildNamespace); + projectFileContents = projectFileContents.Replace("msbuilddefaulttoolsversion", "15.0"); + projectFileContents = projectFileContents.Replace("msbuildassemblyversion", "15.1.0.0"); + + return projectFileContents; + } + } +} diff --git a/src/StructuredLogger.Tests/Roundtrip.cs b/src/StructuredLogger.Tests/Roundtrip.cs index fe367e66..91291f8b 100644 --- a/src/StructuredLogger.Tests/Roundtrip.cs +++ b/src/StructuredLogger.Tests/Roundtrip.cs @@ -62,8 +62,7 @@ public void RoundtripTest() public void ReadBinaryLogRecords() { var reader = new BinaryLogReplayEventSource(); - var records = reader.ReadRecords(@"D:\msbuild.binlog").ToArray(); - var count = records.Length; + var records = reader.ReadRecords(@"C:\temp\msbuild.binlog"); foreach (var record in records) { var t = record.Args; diff --git a/src/StructuredLogger.Tests/StructuredLogger.Tests.csproj b/src/StructuredLogger.Tests/StructuredLogger.Tests.csproj index 933b163d..bc8841e1 100644 --- a/src/StructuredLogger.Tests/StructuredLogger.Tests.csproj +++ b/src/StructuredLogger.Tests/StructuredLogger.Tests.csproj @@ -1,7 +1,7 @@  StructuredLogger.Tests - net46 + net46 true diff --git a/src/StructuredLogger/BinaryLog.cs b/src/StructuredLogger/BinaryLog.cs index 2139c00e..aae1a45f 100644 --- a/src/StructuredLogger/BinaryLog.cs +++ b/src/StructuredLogger/BinaryLog.cs @@ -7,9 +7,22 @@ public class BinaryLog { public static Build ReadBuild(string filePath) { - var eventSource = new BinaryLogReplayEventSource(); + using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + var projectImportsZipFile = Path.ChangeExtension(filePath, ".ProjectImports.zip"); + byte[] projectImportsArchive = null; + if (File.Exists(projectImportsZipFile)) + { + projectImportsArchive = File.ReadAllBytes(projectImportsZipFile); + } - byte[] sourceArchive = null; + return ReadBuild(stream, projectImportsArchive); + } + } + + public static Build ReadBuild(Stream stream, byte[] projectImportsArchive = null) + { + var eventSource = new BinaryLogReplayEventSource(); Build build = null; @@ -17,7 +30,7 @@ public static Build ReadBuild(string filePath) { if (kind == BinaryLogRecordKind.ProjectImportArchive) { - sourceArchive = bytes; + projectImportsArchive = bytes; } }; eventSource.OnException += ex => @@ -37,7 +50,7 @@ public static Build ReadBuild(string filePath) build = structuredLogger.Construction.Build; var sw = Stopwatch.StartNew(); - eventSource.Replay(filePath); + eventSource.Replay(stream); var elapsed = sw.Elapsed; structuredLogger.Shutdown(); @@ -48,16 +61,14 @@ public static Build ReadBuild(string filePath) if (build == null) { build = new Build() { Succeeded = false }; - build.AddChild(new Error() { Text = "Error when opening the file: " + filePath }); + build.AddChild(new Error() { Text = "Error when opening the log file." }); } - var projectImportsZip = Path.ChangeExtension(filePath, ".ProjectImports.zip"); - if (sourceArchive == null && File.Exists(projectImportsZip)) + if (build.SourceFilesArchive == null && projectImportsArchive != null) { - sourceArchive = File.ReadAllBytes(projectImportsZip); + build.SourceFilesArchive = projectImportsArchive; } - build.SourceFilesArchive = sourceArchive; // build.AddChildAtBeginning(new Message { Text = "Elapsed: " + elapsed.ToString() }); return build; diff --git a/src/StructuredLogger/BinaryLogger/BinaryLogReplayEventSource.cs b/src/StructuredLogger/BinaryLogger/BinaryLogReplayEventSource.cs index 27ca99bb..7be7239d 100644 --- a/src/StructuredLogger/BinaryLogger/BinaryLogReplayEventSource.cs +++ b/src/StructuredLogger/BinaryLogger/BinaryLogReplayEventSource.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; @@ -33,60 +34,95 @@ public void Replay(string sourceFilePath) { using (var stream = new FileStream(sourceFilePath, FileMode.Open, FileAccess.Read, FileShare.Read)) { - var gzipStream = new GZipStream(stream, CompressionMode.Decompress, leaveOpen: true); - var binaryReader = new BinaryReader(gzipStream); + Replay(stream); + } + } + + public void Replay(Stream stream) + { + var gzipStream = new GZipStream(stream, CompressionMode.Decompress, leaveOpen: true); + var binaryReader = new BinaryReader(gzipStream); - int fileFormatVersion = binaryReader.ReadInt32(); + int fileFormatVersion = binaryReader.ReadInt32(); - // the log file is written using a newer version of file format - // that we don't know how to read - if (fileFormatVersion > BinaryLogger.FileFormatVersion) + // the log file is written using a newer version of file format + // that we don't know how to read + if (fileFormatVersion > BinaryLogger.FileFormatVersion) + { + var text = $"Unsupported log file format. Latest supported version is {BinaryLogger.FileFormatVersion}, the log file has version {fileFormatVersion}."; + throw new NotSupportedException(text); + } + + // Use a producer-consumer queue so that IO can happen on one thread + // while processing can happen on another thread decoupled. The speed + // up is from 4.65 to 4.15 seconds. + var queue = new BlockingCollection(boundedCapacity: 5000); + var processingTask = System.Threading.Tasks.Task.Run(() => + { + foreach (var args in queue.GetConsumingEnumerable()) { - var text = $"Unsupported log file format. Latest supported version is {BinaryLogger.FileFormatVersion}, the log file has version {fileFormatVersion}."; - throw new NotSupportedException(text); + Dispatch(args); } + }); - // Use a producer-consumer queue so that IO can happen on one thread - // while processing can happen on another thread decoupled. The speed - // up is from 4.65 to 4.15 seconds. - var queue = new BlockingCollection(boundedCapacity: 5000); - var processingTask = System.Threading.Tasks.Task.Run(() => - { - foreach (var args in queue.GetConsumingEnumerable()) - { - Dispatch(args); - } - }); + int recordsRead = 0; + + var reader = new BuildEventArgsReader(binaryReader, fileFormatVersion); + reader.OnBlobRead += OnBlobRead; + while (true) + { + BuildEventArgs instance = null; - int recordsRead = 0; + try + { + instance = reader.Read(); + } + catch (Exception ex) + { + OnException?.Invoke(ex); + } - var reader = new BuildEventArgsReader(binaryReader, fileFormatVersion); - reader.OnBlobRead += OnBlobRead; - while (true) + recordsRead++; + if (instance == null) { - BuildEventArgs instance = null; - - try - { - instance = reader.Read(); - } - catch (Exception ex) - { - OnException?.Invoke(ex); - } - - recordsRead++; - if (instance == null) - { - queue.CompleteAdding(); - break; - } - - queue.Add(instance); + queue.CompleteAdding(); + break; } - processingTask.Wait(); + queue.Add(instance); } + + processingTask.Wait(); + } + + private class DisposableEnumerable : IEnumerable, IDisposable + { + private IEnumerable enumerable; + private Action dispose; + + public static IEnumerable Create(IEnumerable enumerable, Action dispose) + { + return new DisposableEnumerable(enumerable, dispose); + } + + public DisposableEnumerable(IEnumerable enumerable, Action dispose) + { + this.enumerable = enumerable; + this.dispose = dispose; + } + + public void Dispose() + { + if (dispose != null) + { + dispose(); + dispose = null; + } + } + + public IEnumerator GetEnumerator() => enumerable.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => enumerable.GetEnumerator(); } /// @@ -94,81 +130,88 @@ public void Replay(string sourceFilePath) /// the start position in the stream, length in bytes and the deserialized object. /// /// Useful for debugging and analyzing binary logs - public IEnumerable ReadRecords(string sourceFilePath) + public IEnumerable ReadRecords(string logFilePath) { - using (var stream = new FileStream(sourceFilePath, FileMode.Open, FileAccess.Read, FileShare.Read)) - { - var gzipStream = new GZipStream(stream, CompressionMode.Decompress, leaveOpen: true); - var memoryStream = new MemoryStream(); - gzipStream.CopyTo(memoryStream); - memoryStream.Position = 0; + var stream = new FileStream(logFilePath, FileMode.Open, FileAccess.Read, FileShare.Read); + return DisposableEnumerable.Create(ReadRecords(stream), () => stream.Dispose()); + } - var binaryReader = new BinaryReader(memoryStream); - var bytes = memoryStream.ToArray(); + public IEnumerable ReadRecords(byte[] bytes) + { + var stream = new MemoryStream(bytes); + return ReadRecords(stream); + } + + /// + /// Enumerate over all records in the binary log stream. For each record store the bytes, + /// the start position in the stream, length in bytes and the deserialized object. + /// + /// Useful for debugging and analyzing binary logs + public IEnumerable ReadRecords(Stream binaryLogStream) + { + var gzipStream = new GZipStream(binaryLogStream, CompressionMode.Decompress, leaveOpen: true); + return ReadRecordsFromDecompressedStream(gzipStream); + } - int fileFormatVersion = binaryReader.ReadInt32(); + public IEnumerable ReadRecordsFromDecompressedStream(Stream decompressedStream) + { + var binaryReader = new BinaryReader(decompressedStream); - // the log file is written using a newer version of file format - // that we don't know how to read - if (fileFormatVersion > BinaryLogger.FileFormatVersion) - { - var text = $"Unsupported log file format. Latest supported version is {BinaryLogger.FileFormatVersion}, the log file has version {fileFormatVersion}."; - throw new NotSupportedException(text); - } + int fileFormatVersion = binaryReader.ReadInt32(); - long index = memoryStream.Position; - long lengthOfBlobsAddedLastTime = 0; + // the log file is written using a newer version of file format + // that we don't know how to read + if (fileFormatVersion > BinaryLogger.FileFormatVersion) + { + var text = $"Unsupported log file format. Latest supported version is {BinaryLogger.FileFormatVersion}, the log file has version {fileFormatVersion}."; + throw new NotSupportedException(text); + } - List blobs = new List(); + long lengthOfBlobsAddedLastTime = 0; - var reader = new BuildEventArgsReader(binaryReader, fileFormatVersion); - reader.OnBlobRead += (kind, blob) => + List blobs = new List(); + + var reader = new BuildEventArgsReader(binaryReader, fileFormatVersion); + reader.OnBlobRead += (kind, blob) => + { + var record = new Record { - var record = new Record - { - Bytes = blob, - Args = null, - Start = index, - Length = blob.Length - }; - - blobs.Add(record); - lengthOfBlobsAddedLastTime += blob.Length; + Bytes = blob, + Args = null, + Start = 0, // TODO: see if we can re-add that + Length = blob.Length }; - while (true) - { - BuildEventArgs instance = null; + blobs.Add(record); + lengthOfBlobsAddedLastTime += blob.Length; + }; - instance = reader.Read(); - if (instance == null) - { - break; - } - - var position = memoryStream.Position; - var length = position - index - lengthOfBlobsAddedLastTime; - - var chunk = new byte[length]; - Array.Copy(bytes, (int)(index + lengthOfBlobsAddedLastTime), chunk, 0, (int)length); - var record = new Record - { - Bytes = chunk, - Args = instance, - Start = index, - Length = length - }; - - yield return record; - - index = position; - lengthOfBlobsAddedLastTime = 0; - } + while (true) + { + BuildEventArgs instance = null; - foreach (var blob in blobs) + instance = reader.Read(); + if (instance == null) { - yield return blob; + break; } + + var record = new Record + { + Bytes = null, // probably can reconstruct this from the Args if necessary + Args = instance, + Start = 0, + Length = 0 + }; + + yield return record; + + lengthOfBlobsAddedLastTime = 0; + } + + foreach (var blob in blobs) + { + yield return blob; } } } diff --git a/src/StructuredLogger/BinaryLogger/ProjectImportsCollector.cs b/src/StructuredLogger/BinaryLogger/ProjectImportsCollector.cs index 2f24abcc..70498d94 100644 --- a/src/StructuredLogger/BinaryLogger/ProjectImportsCollector.cs +++ b/src/StructuredLogger/BinaryLogger/ProjectImportsCollector.cs @@ -47,7 +47,7 @@ public ProjectImportsCollector(string logFilePath, string sourcesArchiveExtensio public void AddFile(string filePath) { - if (filePath == null || _fileStream == null) + if (string.IsNullOrEmpty(filePath) || _fileStream == null) { return; } diff --git a/src/StructuredLogger/Serialization/Binary/BinaryLogReader.cs b/src/StructuredLogger/Serialization/Binary/BinaryLogReader.cs index cc06382a..9c7f9217 100644 --- a/src/StructuredLogger/Serialization/Binary/BinaryLogReader.cs +++ b/src/StructuredLogger/Serialization/Binary/BinaryLogReader.cs @@ -6,7 +6,6 @@ namespace Microsoft.Build.Logging.StructuredLogger { public class BinaryLogReader : IDisposable { - private readonly string filePath; private TreeBinaryReader reader; private readonly Queue attributes = new Queue(10); @@ -17,7 +16,22 @@ public class BinaryLogReader : IDisposable public static Build Read(string filePath) { - using (var binaryLogReader = new BinaryLogReader(filePath)) + using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + var projectImportsZipFile = Path.ChangeExtension(filePath, ".ProjectImports.zip"); + byte[] projectImportsArchive = null; + if (File.Exists(projectImportsZipFile)) + { + projectImportsArchive = File.ReadAllBytes(projectImportsZipFile); + } + + return Read(stream, projectImportsArchive); + } + } + + public static Build Read(Stream stream, byte[] projectImportsArchive = null) + { + using (var binaryLogReader = new BinaryLogReader(stream)) { if (!binaryLogReader.formatIsValid) { @@ -32,10 +46,9 @@ public static Build Read(string filePath) buildStringCache.Intern(stringInstance); } - var projectImportsZip = Path.ChangeExtension(filePath, ".ProjectImports.zip"); - if (build.SourceFilesArchive == null && File.Exists(projectImportsZip)) + if (build.SourceFilesArchive == null && projectImportsArchive != null) { - build.SourceFilesArchive = File.ReadAllBytes(projectImportsZip); + build.SourceFilesArchive = projectImportsArchive; } return build; @@ -44,7 +57,6 @@ public static Build Read(string filePath) private BinaryLogReader(string filePath) { - this.filePath = filePath; this.reader = new TreeBinaryReader(filePath); this.formatSupportsSourceFiles = reader.Version > new Version(1, 0, 130); this.formatSupportsEmbeddedProjectImportsArchive = reader.Version > new Version(1, 1, 87); @@ -52,6 +64,15 @@ private BinaryLogReader(string filePath) this.formatIsValid = reader.IsValid(); } + private BinaryLogReader(Stream stream) + { + this.reader = new TreeBinaryReader(stream); + this.formatSupportsSourceFiles = reader.Version > new Version(1, 0, 130); + this.formatSupportsEmbeddedProjectImportsArchive = reader.Version > new Version(1, 1, 87); + this.formatSupportsTimedNodeId = reader.Version > new Version(1, 1, 153); + this.formatIsValid = reader.IsValid(); + } + private object ReadNode() { var name = reader.ReadString(); diff --git a/src/StructuredLogger/Serialization/Binary/TreeBinaryReader.cs b/src/StructuredLogger/Serialization/Binary/TreeBinaryReader.cs index 5c1e41e9..1ed3a4e0 100644 --- a/src/StructuredLogger/Serialization/Binary/TreeBinaryReader.cs +++ b/src/StructuredLogger/Serialization/Binary/TreeBinaryReader.cs @@ -7,17 +7,25 @@ namespace Microsoft.Build.Logging.StructuredLogger { public class TreeBinaryReader : IDisposable { - private readonly string filePath; private BinaryReader binaryReader; - private readonly FileStream fileStream; - private readonly GZipStream gzipStream; - private readonly string[] stringTable; - private readonly List attributes = new List(10); + private Stream fileStream; + private GZipStream gzipStream; + private string[] stringTable; public TreeBinaryReader(string filePath) { - this.filePath = filePath; - this.fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read); + this.fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); + Initialize(fileStream); + } + + public TreeBinaryReader(Stream stream) + { + Initialize(stream); + } + + private void Initialize(Stream stream) + { + this.fileStream = stream; if (fileStream.Length < 8) { // file is too short to be valid @@ -129,6 +137,12 @@ public void Dispose() binaryReader.Dispose(); binaryReader = null; } + + if (fileStream != null) + { + fileStream.Dispose(); + fileStream = null; + } } } } diff --git a/src/StructuredLogger/Serialization/Serialization.cs b/src/StructuredLogger/Serialization/Serialization.cs index 5dd35fa6..2b74f4d6 100644 --- a/src/StructuredLogger/Serialization/Serialization.cs +++ b/src/StructuredLogger/Serialization/Serialization.cs @@ -39,6 +39,10 @@ public static Dictionary ObjectModelTypes } } + public static Build ReadXmlLog(Stream stream) => XmlLogReader.ReadFromXml(stream); + public static Build ReadBuildLog(Stream stream, byte[] projectImportsArchive = null) => BinaryLogReader.Read(stream, projectImportsArchive); + public static Build ReadBinLog(Stream stream, byte[] projectImportsArchive = null) => BinaryLog.ReadBuild(stream, projectImportsArchive); + public static Build Read(string filePath) { if (filePath.EndsWith(".xml", StringComparison.OrdinalIgnoreCase)) diff --git a/src/StructuredLogger/Serialization/XmlLogReader.cs b/src/StructuredLogger/Serialization/XmlLogReader.cs index b957d746..19686db1 100644 --- a/src/StructuredLogger/Serialization/XmlLogReader.cs +++ b/src/StructuredLogger/Serialization/XmlLogReader.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Xml; namespace Microsoft.Build.Logging.StructuredLogger @@ -15,7 +16,20 @@ public static Build ReadFromXml(string xmlFilePath) return new XmlLogReader().Read(xmlFilePath); } + public static Build ReadFromXml(Stream stream) + { + return new XmlLogReader().Read(stream); + } + public Build Read(string filePath) + { + using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + return Read(stream); + } + } + + public Build Read(Stream stream) { Build build = new Build(); this.stringTable = build.StringTable; @@ -23,6 +37,8 @@ public Build Read(string filePath) var stack = new Stack(1024); stack.Push(build); + XmlNodeType previous = XmlNodeType.None; + try { var xmlReaderSettings = new XmlReaderSettings() @@ -30,7 +46,7 @@ public Build Read(string filePath) IgnoreWhitespace = true, }; - using (reader = XmlReader.Create(filePath, xmlReaderSettings)) + using (reader = XmlReader.Create(stream, xmlReaderSettings)) { reader.MoveToContent(); @@ -39,6 +55,7 @@ public Build Read(string filePath) while (reader.Read()) { + var nodeType = reader.NodeType; switch (reader.NodeType) { case XmlNodeType.Element: @@ -54,25 +71,23 @@ public Build Read(string filePath) break; case XmlNodeType.EndElement: - stack.Pop(); - break; - case XmlNodeType.Text: { - var valueNode = stack.Peek(); - var nameValueNode = valueNode as NameValueNode; - if (nameValueNode != null) + // if the element content is an empty string + if (previous == XmlNodeType.Element) { - nameValueNode.Value = GetCurrentValue(); - } - else - { - var message = valueNode as Message; - if (message != null) - { - message.Text = GetCurrentValue(); - } + var valueNode = stack.Peek(); + SetElementValue(valueNode, ""); } + stack.Pop(); + break; + } + case XmlNodeType.Text: + { + var valueNode = stack.Peek(); + string value = reader.Value; + SetElementValue(valueNode, stringTable.Intern(value)); + break; } case XmlNodeType.Whitespace: @@ -111,19 +126,33 @@ public Build Read(string filePath) default: break; } + + previous = nodeType; } } } catch (Exception ex) { build = new Build() { Succeeded = false }; - build.AddChild(new Error() { Text = "Error when opening file: " + filePath }); + build.AddChild(new Error() { Text = "Error when opening XML log file." }); build.AddChild(new Error() { Text = ex.ToString() }); } return build; } + private void SetElementValue(object valueNode, string value) + { + if (valueNode is NameValueNode nameValueNode) + { + nameValueNode.Value = value; + } + else if (valueNode is Message message) + { + message.Text = value; + } + } + private string GetCurrentValue() { return stringTable.Intern(reader.Value);