diff --git a/build/Rulesets/Roslyn_BuildRules.ruleset b/build/Rulesets/Roslyn_BuildRules.ruleset index 7f3321dcedd886352f3e5e09e079d5a4d7c3f047..b6abab47bbd5fafd85563f15a82baece7210372c 100644 --- a/build/Rulesets/Roslyn_BuildRules.ruleset +++ b/build/Rulesets/Roslyn_BuildRules.ruleset @@ -162,6 +162,7 @@ collection. + diff --git a/netci.groovy b/netci.groovy index e21956f1faaf10f33e11f516c896722d26232389..4c44dfbf69b1fe4f0155c540fd5582cbb175be1c 100644 --- a/netci.groovy +++ b/netci.groovy @@ -223,7 +223,7 @@ commitPullList.each { isPr -> def myJob = job(jobName) { description("Windows ${configuration} tests on ${buildTarget}") steps { - batchFile(""".\\build\\scripts\\cibuild.cmd -${configuration} -procdump -testVsi""") + batchFile(""".\\build\\scripts\\cibuild.cmd -${configuration} -testVsi""") } } diff --git a/src/VisualStudio/IntegrationTest/TestUtilities/EventLogCollector.cs b/src/VisualStudio/IntegrationTest/TestUtilities/EventLogCollector.cs new file mode 100644 index 0000000000000000000000000000000000000000..2467279b72e28fa0e8668aae284d4c222c03116c --- /dev/null +++ b/src/VisualStudio/IntegrationTest/TestUtilities/EventLogCollector.cs @@ -0,0 +1,317 @@ +// Copyright (c) Microsoft. 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.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.Eventing.Reader; +using System.IO; +using System.Linq; +using System.Text; + +namespace Microsoft.VisualStudio.IntegrationTest.Utilities +{ + /// + /// Helper class to read the Application Event Log for Watson and .NetRuntime entries + /// + internal static class EventLogCollector + { + /// + /// The name of the Event Log to query + /// + private const string EventLogName = "Application"; + + /// + /// We want to get either the entries for the past day or the last 5 (whichever has a greater count) + /// + private const int MinimumEntries = 5; + + /// + /// We want to get either the entries for the past day or the last 5 (whichever has a greater count) + /// + private const int DaysToGetEventsFor = 1; + + /// + /// We don't want to add events that are older than a week + /// + private const int MaxDaysToGetEventsFor = 7; + + /// + /// For Watson, the Provider Name in the Event Log is "Windows Error Reporting" + /// + private const string WatsonProviderName = "Windows Error Reporting"; + + /// + /// For Watson, the Event Id in the Event Log that we are interested in is 1001 + /// + private const int WatsonEventId = 1001; + + /// + /// Each entry in the EventLog has 22 Properties: 0-bucketId, 1-eventTypeId, 2-eventName, 3-response, 4-cabId, 5:14-bucketParameters P1:P10, + /// 15-attachedFiles, 16-location, 17-analysisSymbol, 18-recheck, 19-reportId, 20-reportStatus, 21-hashedBucket + /// + private const int WatsonEventLogEntryPropertyCount = 22; + + /// + /// FaultBucket is the first property on the log entry + /// + private const int FaultBucketIndex = 0; + + /// + /// For .DotNetRuntime, the Provider Name in the Event Log + /// + private const string DotNetProviderName = ".NET Runtime"; + + /// + /// The Event Id in the Event Log for .DotNetRuntime that we want to scope down to + /// 1023 - ERT_UnmanagedFailFast, 1025 - ERT_ManagedFailFast, 1026 - ERT_UnhandledException, 1027 - ERT_StackOverflow, 1028 - ERT_CodeContractFailed + /// + private static readonly ImmutableArray s_dotNetEventId = ImmutableArray.Create(1023, 1024, 1025, 1026, 1027, 1028); + + /// + /// List of EventNames to exclude from our search in the Event Log + /// + internal static HashSet ExcludedEventNames = new HashSet() + { + "VisualStudioNonFatalErrors", + "VisualStudioNonFatalErrors2" + }; + + /// + /// List of VS EXEs to search in the Event Log for + /// + internal static HashSet VsRelatedExes = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "devenv.exe", + "csc.exe", + "csi.exe", + "git.exe", + "msbuild.exe", + "MSBuildTaskHost.exe", + "mspdbsrv.exe", + "MStest.exe", + "ServiceHub.Host.CLR.exe", + "ServiceHub.Host.CLR.x64.exe", + "ServiceHub.Host.CLR.x86.exe", + "ServiceHub.IdentityHost.exe", + "ServiceHub.RoslynCodeAnalysisService32.exe", + "ServiceHub.SettingsHost.exe", + "ServiceHub.VSDetouredHost.exe", + "vbc.exe", + "vbc2.exe", + "VBCSCompiler.exe", + "VStest.Console.Exe", + "VSTest.DiscoveryEngine.exe", + "VSTest.DiscoveryEngine.x86.exe", + "vstest.executionengine.appcontainer.exe", + "vstest.executionengine.appcontainer.x86.exe", + "vstest.executionengine.clr20.exe", + "VSTest.executionEngine.exe", + "VSTest.executionEngine.x86.exe", + }; + + /// + /// Get the WER entries for VS and VS related EXEs from the Event Log and write them to a file + /// + internal static void TryWriteWatsonEntriesToFile(string filePath) + { + try + { + // Use a HashSet to make sure the entries we add aren't duplicates (calls the Equals override from FeedbackItemWatsonEntry) + var watsonEntries = new HashSet(); + + // We need to search in the Application Event Log, since that's where Watson logs entries + var eventLogQuery = new EventLogQuery(EventLogName, PathType.LogName) + { + // Read events in descending order, so we can get either the last 5 entries or the past day of entries, whichever has a bigger count + ReverseDirection = true + }; + + var eventLogReader = new EventLogReader(eventLogQuery); + EventRecord eventLogRecord; + var watsonEntriesCount = 0; + while ((eventLogRecord = eventLogReader.ReadEvent()) != null) + { + // We only want the last 5 entries or the past day of entries, whichever has a bigger count + if (IsLastDayOrLastFiveRecentEntry(eventLogRecord, watsonEntriesCount)) + { + // Filter the entries by Watson specific ones for VS EXEs + if (IsValidWatsonEntry(eventLogRecord)) + { + var entry = new FeedbackItemWatsonEntry(eventLogRecord); + watsonEntries.Add(entry); + + // If the entry doesn't have a valid BucketId, we don't want it to count towards the maxCount we send + if (!string.IsNullOrWhiteSpace(GetEventRecordPropertyToString(eventLogRecord, FaultBucketIndex))) + { + watsonEntriesCount++; + } + } + } + else + { + break; + } + } + + if (watsonEntries.Any()) + { + var watsonEntriesStringBuilder = new StringBuilder(); + foreach (var entry in watsonEntries) + { + watsonEntriesStringBuilder.AppendLine($"Event Time (UTC): {entry.EventTime}"); + watsonEntriesStringBuilder.AppendLine($"Application Name: {entry.ApplicationName}"); + watsonEntriesStringBuilder.AppendLine($"Application Version: {entry.ApplicationVersion}"); + watsonEntriesStringBuilder.AppendLine($"Faulting Module: {entry.FaultingModule}"); + watsonEntriesStringBuilder.AppendLine($"Faulting Module Version: {entry.FaultingModuleVersion}"); + watsonEntriesStringBuilder.AppendLine($"Event Name: {entry.EventName}"); + watsonEntriesStringBuilder.AppendLine($"Cab Id: {entry.CabId}"); + watsonEntriesStringBuilder.AppendLine($"Fault Bucket: {entry.FaultBucket}"); + watsonEntriesStringBuilder.AppendLine($"Hashed Bucket: {entry.HashedBucket}"); + watsonEntriesStringBuilder.AppendLine($"Watson Report Id: {entry.WatsonReportId}"); + watsonEntriesStringBuilder.AppendLine(); + } + + File.WriteAllText(filePath, watsonEntriesStringBuilder.ToString()); + } + } + catch (Exception ex) + { + File.WriteAllText(filePath, ex.ToString()); + } + } + + /// + /// Get the .NetRuntime entries from the Event Log and write them to a file + /// + internal static void TryWriteDotNetEntriesToFile(string filePath) + { + try + { + var dotNetEntries = new HashSet(); + + // We need to search in the Application Event Log, since that's where .NetRuntime logs entries + var eventLogQuery = new EventLogQuery(EventLogName, PathType.LogName) + { + // Read events in descending order, so we can get either the last 5 entries or the past day of entries, whichever has a bigger count + ReverseDirection = true + }; + + var eventLogReader = new EventLogReader(eventLogQuery); + EventRecord eventLogRecord; + while ((eventLogRecord = eventLogReader.ReadEvent()) != null) + { + // We only want the last 5 entries or the past day of entries, whichever has a bigger count + if (IsLastDayOrLastFiveRecentEntry(eventLogRecord, dotNetEntries.Count)) + { + // Filter the entries by .NetRuntime specific ones + FeedbackItemDotNetEntry entry = null; + if (IsValidDotNetEntry(eventLogRecord, ref entry)) + { + dotNetEntries.Add(entry); + } + } + else + { + break; + } + } + + if (dotNetEntries.Any()) + { + var dotNetEntriesStringBuilder = new StringBuilder(); + foreach (var entry in dotNetEntries) + { + dotNetEntriesStringBuilder.AppendLine($"Event Time (UTC): {entry.EventTime}"); + dotNetEntriesStringBuilder.AppendLine($"Event ID: {entry.EventId}"); + dotNetEntriesStringBuilder.AppendLine($"Data: {entry.Data.Replace("\n", "\r\n")}"); + dotNetEntriesStringBuilder.AppendLine(); + } + + File.WriteAllText(filePath, dotNetEntriesStringBuilder.ToString()); + } + } + catch (Exception ex) + { + File.WriteAllText(filePath, ex.ToString()); + } + } + + /// + /// Returns true if this is one of the last 5 entries over the past week or the past day of entries, whichever has a bigger count + /// + /// Event entry to be checked + /// List of already valid entries + private static bool IsLastDayOrLastFiveRecentEntry(EventRecord eventLogRecord, int entriesCount) + { + // This is local time (it will be later converted to UTC when we send the feedback) + if (eventLogRecord.TimeCreated.HasValue + && (eventLogRecord.TimeCreated.Value > DateTime.Now.AddDays(-MaxDaysToGetEventsFor)) + && ((eventLogRecord.TimeCreated.Value > DateTime.Now.AddDays(-DaysToGetEventsFor)) || (entriesCount < MinimumEntries))) + { + return true; + } + + return false; + } + + /// + /// Verifies if an entry is a valid Watson one by checking: + /// the provider, if it's for VS EXEs or the installer EXEs, and it's not a VisualStudioNonFatalErrors or VisualStudioNonFatalErrors2 + /// + /// Entry to be checked + private static bool IsValidWatsonEntry(EventRecord eventLogRecord) + { + if (StringComparer.InvariantCultureIgnoreCase.Equals(eventLogRecord.ProviderName, WatsonProviderName) + && (eventLogRecord.Id == WatsonEventId) + && (eventLogRecord.Properties.Count >= WatsonEventLogEntryPropertyCount) + && (!ExcludedEventNames.Contains(GetEventRecordPropertyToString(eventLogRecord, FeedbackItemWatsonEntry.EventNameIndex))) + && VsRelatedExes.Contains(GetEventRecordPropertyToString(eventLogRecord, FeedbackItemWatsonEntry.ApplicationNameIndex))) + { + return true; + } + + return false; + } + + /// + /// Verifies if an entry is a valid .NET one by checking: + /// the provider, if it's for certain event log IDs and for VS related EXEs + /// + /// Entry to be checked + private static bool IsValidDotNetEntry(EventRecord eventLogRecord, ref FeedbackItemDotNetEntry dotNetEntry) + { + if (StringComparer.InvariantCultureIgnoreCase.Equals(eventLogRecord.ProviderName, DotNetProviderName) + && s_dotNetEventId.Contains(eventLogRecord.Id)) + { + dotNetEntry = new FeedbackItemDotNetEntry(eventLogRecord); + foreach (var app in VsRelatedExes) + { + if (dotNetEntry.Data.IndexOf(app, StringComparison.InvariantCultureIgnoreCase) >= 0) + { + return true; + } + } + } + + return false; + } + + /// + /// Given the EventRecord and the index in it, get its value as a string (empty if it's null) + /// + /// EventRecord + /// Index in the EventRecord + /// string if not null or string.Empty + internal static string GetEventRecordPropertyToString(EventRecord eventLogRecord, int index) + { + if (eventLogRecord.Properties[index].Value == null) + { + return string.Empty; + } + else + { + return eventLogRecord.Properties[index].Value.ToString(); + } + } + } +} diff --git a/src/VisualStudio/IntegrationTest/TestUtilities/FeedbackItemDotNetEntry.cs b/src/VisualStudio/IntegrationTest/TestUtilities/FeedbackItemDotNetEntry.cs new file mode 100644 index 0000000000000000000000000000000000000000..fdf2d010b074f8723d88f375b3a8c3bc7ce4a86d --- /dev/null +++ b/src/VisualStudio/IntegrationTest/TestUtilities/FeedbackItemDotNetEntry.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft. 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.Diagnostics.Eventing.Reader; +using System.Linq; +using System.Runtime.Serialization; + +namespace Microsoft.VisualStudio.IntegrationTest.Utilities +{ + /// + /// Mapper for the .NetRuntime entry in the Event Log + /// + [DataContract] + internal class FeedbackItemDotNetEntry + { + /// + /// The time the event happend (UTC) + /// + [DataMember(Name = "eventTime")] + public DateTime EventTime { get; set; } + + /// + /// The .NET Runtime event id (this is set by .NET and we get it from the Event Log, so we can better differenciate between them) + /// As defined in CLR code: ndp\clr\src\vm\eventreporter.cpp, these IDs are: + /// 1023 - ERT_UnmanagedFailFast, 1025 - ERT_ManagedFailFast, 1026 - ERT_UnhandledException, 1027 - ERT_StackOverflow, 1028 - ERT_CodeContractFailed + /// + [DataMember(Name = "eventId")] + public int EventId { get; set; } + + /// + /// The event log properties to be passed as one string. E.g. + /// Application: CSAv.exe, Framework version: v4.0.30319, + /// Description: The application requested termination through System.Environment.FailFast(string message) + /// Stack: at CSAv.Program.GetModuleFileName(IntPtr, Int32, Int32) + /// + [DataMember(Name = "data")] + public string Data { get; set; } + + /// + /// Constructor for the FeedbackItemDotNetEntry based on an EventRecord from the EventLog + /// + public FeedbackItemDotNetEntry(EventRecord eventLogRecord) + { + EventTime = eventLogRecord.TimeCreated.Value.ToUniversalTime(); + EventId = eventLogRecord.Id; + Data = string.Join(";", eventLogRecord.Properties.Select(pr => pr.Value ?? string.Empty)); + } + + /// + /// Used to make sure we aren't adding dupe entries to the list of Watson entries + /// + public override bool Equals(object obj) + { + if ((obj is FeedbackItemDotNetEntry dotNetEntry) + && (EventId == dotNetEntry.EventId) + && (Data == dotNetEntry.Data)) + { + return true; + } + + return false; + } + + public override int GetHashCode() + { + return EventId.GetHashCode() ^ Data.GetHashCode(); + } + } +} diff --git a/src/VisualStudio/IntegrationTest/TestUtilities/FeedbackItemWatsonEntry.cs b/src/VisualStudio/IntegrationTest/TestUtilities/FeedbackItemWatsonEntry.cs new file mode 100644 index 0000000000000000000000000000000000000000..b776820cc33cbb1b77e3ff7d628d2ca049ce43e7 --- /dev/null +++ b/src/VisualStudio/IntegrationTest/TestUtilities/FeedbackItemWatsonEntry.cs @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft. 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.Diagnostics.Eventing.Reader; +using System.Runtime.Serialization; + +namespace Microsoft.VisualStudio.IntegrationTest.Utilities +{ + /// + /// Mapper for the Watson entry in the Event Log + /// + [DataContract] + internal class FeedbackItemWatsonEntry + { + /// + /// The time the event happend (UTC) + /// + [DataMember(Name = "eventTime")] + public DateTime EventTime { get; } + + /// + /// Bucket Id + /// + [DataMember(Name = "faultBucket")] + public string FaultBucket { get; } + + /// + /// Bucket Hash (might replace ID which would be deprecated, so sending for future proofing) + /// + [DataMember(Name = "hashedBucket")] + public string HashedBucket { get; } + + /// + /// Watson Report ID + /// + [DataMember(Name = "watsonReportId")] + public string WatsonReportId { get; } + + /// + /// The name of the event (some possible ones: "AppHangB1", "AppHangXProcB1", "MoAppHang","MoAppHangXProc","AppCrash","Crash32","Crash64","MoAppCrash","BEX","BEX64","clr20r3","MoBEX" + /// + [DataMember(Name = "eventName")] + public string EventName { get; } + + /// + /// The CAB unique ID (can be empty - 0) + /// + [DataMember(Name = "cabId")] + public string CabId { get; } + + /// + /// The name of the application causing the event (we have a list of VS EXEs that we grab for), e.g. "devenv.exe" + /// + [DataMember(Name = "applicationName")] + public string ApplicationName { get; } + + /// + /// The version of the application causing the event, e.g. "14.0.23107.0" + /// + [DataMember(Name = "applicationVersion")] + public string ApplicationVersion { get; } + + /// + /// The faulting module (what inside the app is causing the event), e.g. "ntdll.dll" + /// + [DataMember(Name = "faultingModule")] + public string FaultingModule { get; } + + /// + /// The faulting module version + /// + [DataMember(Name = "faultModuleVersion")] + public string FaultingModuleVersion { get; } + + /// + /// FaultBucket is the first property on the log entry + /// + private const int FaultBucketIndex = 0; + + /// + /// EventName index in the log entry properties (2) + /// + internal const int EventNameIndex = 2; + + /// + /// CabId index in the log entry properties + /// + private const int CabIdIndex = 4; + + /// + /// Application name is contained in the P1 bucket parameter + /// + internal const int ApplicationNameIndex = 5; + + /// + /// Application version is the P2 bucket parameter + /// + private const int ApplicationVersionIndex = 6; + + /// + /// Faulting module is the P4 bucket parameter + /// + private const int FaultingModuleIndex = 8; + + /// + /// Faulting module version is the P5 bucket parameter + /// + private const int FaultingModuleVersionindex = 9; + + /// + /// WatsonReportId index in the log entry properties + /// + private const int WatsonReportIdIndex = 19; + + /// + /// HashedBucket index in the log entry properties + /// + private const int HashedBucketIndex = 21; + + /// + /// Constructor for a FeedbackItemWatsonEntry based on an EventRecord for future easiness of reading and modifying + /// + public FeedbackItemWatsonEntry(EventRecord eventLogRecord) + { + EventTime = eventLogRecord.TimeCreated.Value.ToUniversalTime(); + FaultBucket = EventLogCollector.GetEventRecordPropertyToString(eventLogRecord, FaultBucketIndex); + HashedBucket = EventLogCollector.GetEventRecordPropertyToString(eventLogRecord, HashedBucketIndex); + WatsonReportId = EventLogCollector.GetEventRecordPropertyToString(eventLogRecord, WatsonReportIdIndex); + EventName = EventLogCollector.GetEventRecordPropertyToString(eventLogRecord, EventNameIndex); + CabId = EventLogCollector.GetEventRecordPropertyToString(eventLogRecord, CabIdIndex); + ApplicationName = EventLogCollector.GetEventRecordPropertyToString(eventLogRecord, ApplicationNameIndex); + ApplicationVersion = EventLogCollector.GetEventRecordPropertyToString(eventLogRecord, ApplicationVersionIndex); + FaultingModule = EventLogCollector.GetEventRecordPropertyToString(eventLogRecord, FaultingModuleIndex); + FaultingModuleVersion = EventLogCollector.GetEventRecordPropertyToString(eventLogRecord, FaultingModuleVersionindex); + } + + /// + /// Used to make sure we aren't adding dupe entries to the list of Watson entries + /// + public override bool Equals(object obj) + { + if ((obj is FeedbackItemWatsonEntry watsonEntry) + && (EventName == watsonEntry.EventName) + && (ApplicationName == watsonEntry.ApplicationName) + && (ApplicationVersion == watsonEntry.ApplicationVersion) + && (FaultingModule == watsonEntry.FaultingModule) + && (FaultingModuleVersion == watsonEntry.FaultingModuleVersion)) + { + return true; + } + + return false; + } + + public override int GetHashCode() + { + return EventName.GetHashCode() ^ ApplicationName.GetHashCode() ^ ApplicationVersion.GetHashCode() ^ FaultingModule.GetHashCode() ^ FaultingModuleVersion.GetHashCode(); + } + } +} diff --git a/src/VisualStudio/IntegrationTest/TestUtilities/VisualStudioInstanceFactory.cs b/src/VisualStudio/IntegrationTest/TestUtilities/VisualStudioInstanceFactory.cs index f325e0bfe3eb71f75c238380b5f70845f65af5d5..19ba8a5b042e31d1baf96916528ca58a3b6ae713 100644 --- a/src/VisualStudio/IntegrationTest/TestUtilities/VisualStudioInstanceFactory.cs +++ b/src/VisualStudio/IntegrationTest/TestUtilities/VisualStudioInstanceFactory.cs @@ -75,6 +75,8 @@ private static void FirstChanceExceptionHandler(object sender, FirstChanceExcept Path.Combine(logDir, $"{baseFileName}.log"), $"{exception}.GetType().Name{Environment.NewLine}{exception.StackTrace}"); + EventLogCollector.TryWriteDotNetEntriesToFile(Path.Combine(logDir, $"{baseFileName}.DotNet.log")); + EventLogCollector.TryWriteWatsonEntriesToFile(Path.Combine(logDir, $"{baseFileName}.Watson.log")); } finally {