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
{