/* **************************************************************************** * * Copyright (c) Microsoft Corporation. * * This source code is subject to terms and conditions of the Apache License, Version 2.0. A * copy of the license can be found in the License.html file at the root of this distribution. If * you cannot locate the Apache License, Version 2.0, please send an email to * vspython@microsoft.com. By using this source code in any fashion, you are agreeing to be bound * by the terms of the Apache License, Version 2.0. * * You must not remove this notice, or any other, from this software. * * ***************************************************************************/ using System; using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text; using System.Threading; using System.Windows.Forms.Design; using System.Windows.Threading; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; using Microsoft.Win32; using IOleServiceProvider = Microsoft.VisualStudio.OLE.Interop.IServiceProvider; namespace Microsoft.VisualStudio.Project { /// /// This class implements an MSBuild logger that output events to VS outputwindow and tasklist. /// [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "IDE")] public class IDEBuildLogger : Logger { #region fields // TODO: Remove these constants when we have a version that suppoerts getting the verbosity using automation. private string buildVerbosityRegistryRoot = @"Software\Microsoft\VisualStudio\10.0"; private const string buildVerbosityRegistrySubKey = @"General"; private const string buildVerbosityRegistryKey = "MSBuildLoggerVerbosity"; private int currentIndent; private IVsOutputWindowPane outputWindowPane; private string errorString = SR.GetString(SR.Error, CultureInfo.CurrentUICulture); private string warningString = SR.GetString(SR.Warning, CultureInfo.CurrentUICulture); private TaskProvider taskProvider; private IVsHierarchy hierarchy; private IServiceProvider serviceProvider; private Dispatcher dispatcher; private bool haveCachedVerbosity = false; // Queues to manage Tasks and Error output plus message logging private ConcurrentQueue> taskQueue; private ConcurrentQueue outputQueue; #endregion #region properties public IServiceProvider ServiceProvider { get { return this.serviceProvider; } } public string WarningString { get { return this.warningString; } set { this.warningString = value; } } public string ErrorString { get { return this.errorString; } set { this.errorString = value; } } /// /// When the build is not a "design time" (background or secondary) build this is True /// /// /// The only known way to detect an interactive build is to check this.outputWindowPane for null. /// protected bool InteractiveBuild { get { return this.outputWindowPane != null; } } /// /// When building from within VS, setting this will /// enable the logger to retrive the verbosity from /// the correct registry hive. /// public string BuildVerbosityRegistryRoot { get { return this.buildVerbosityRegistryRoot; } set { this.buildVerbosityRegistryRoot = value; } } /// /// Set to null to avoid writing to the output window /// public IVsOutputWindowPane OutputWindowPane { get { return this.outputWindowPane; } set { this.outputWindowPane = value; } } #endregion #region ctors /// /// Constructor. Inititialize member data. /// public IDEBuildLogger(IVsOutputWindowPane output, TaskProvider taskProvider, IVsHierarchy hierarchy) { if (taskProvider == null) throw new ArgumentNullException("taskProvider"); if (hierarchy == null) throw new ArgumentNullException("hierarchy"); Trace.WriteLineIf(Thread.CurrentThread.GetApartmentState() != ApartmentState.STA, "WARNING: IDEBuildLogger constructor running on the wrong thread."); IOleServiceProvider site; Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(hierarchy.GetSite(out site)); this.taskProvider = taskProvider; this.outputWindowPane = output; this.hierarchy = hierarchy; this.serviceProvider = new ServiceProvider(site); this.dispatcher = Dispatcher.CurrentDispatcher; } #endregion #region overridden methods /// /// Overridden from the Logger class. /// public override void Initialize(IEventSource eventSource) { if (null == eventSource) { throw new ArgumentNullException("eventSource"); } this.taskQueue = new ConcurrentQueue>(); this.outputQueue = new ConcurrentQueue(); eventSource.BuildStarted += new BuildStartedEventHandler(BuildStartedHandler); eventSource.BuildFinished += new BuildFinishedEventHandler(BuildFinishedHandler); eventSource.ProjectStarted += new ProjectStartedEventHandler(ProjectStartedHandler); eventSource.ProjectFinished += new ProjectFinishedEventHandler(ProjectFinishedHandler); eventSource.TargetStarted += new TargetStartedEventHandler(TargetStartedHandler); eventSource.TargetFinished += new TargetFinishedEventHandler(TargetFinishedHandler); eventSource.TaskStarted += new TaskStartedEventHandler(TaskStartedHandler); eventSource.TaskFinished += new TaskFinishedEventHandler(TaskFinishedHandler); eventSource.CustomEventRaised += new CustomBuildEventHandler(CustomHandler); eventSource.ErrorRaised += new BuildErrorEventHandler(ErrorHandler); eventSource.WarningRaised += new BuildWarningEventHandler(WarningHandler); eventSource.MessageRaised += new BuildMessageEventHandler(MessageHandler); } #endregion #region event delegates /// /// This is the delegate for BuildStartedHandler events. /// protected virtual void BuildStartedHandler(object sender, BuildStartedEventArgs buildEvent) { // NOTE: This may run on a background thread! ClearCachedVerbosity(); ClearQueuedOutput(); ClearQueuedTasks(); QueueOutputEvent(MessageImportance.Low, buildEvent); } /// /// This is the delegate for BuildFinishedHandler events. /// /// /// protected virtual void BuildFinishedHandler(object sender, BuildFinishedEventArgs buildEvent) { // NOTE: This may run on a background thread! MessageImportance importance = buildEvent.Succeeded ? MessageImportance.Low : MessageImportance.High; QueueOutputText(importance, Environment.NewLine); QueueOutputEvent(importance, buildEvent); // flush output and error queues ReportQueuedOutput(); ReportQueuedTasks(); } /// /// This is the delegate for ProjectStartedHandler events. /// protected virtual void ProjectStartedHandler(object sender, ProjectStartedEventArgs buildEvent) { // NOTE: This may run on a background thread! QueueOutputEvent(MessageImportance.Low, buildEvent); } /// /// This is the delegate for ProjectFinishedHandler events. /// protected virtual void ProjectFinishedHandler(object sender, ProjectFinishedEventArgs buildEvent) { // NOTE: This may run on a background thread! QueueOutputEvent(buildEvent.Succeeded ? MessageImportance.Low : MessageImportance.High, buildEvent); } /// /// This is the delegate for TargetStartedHandler events. /// protected virtual void TargetStartedHandler(object sender, TargetStartedEventArgs buildEvent) { // NOTE: This may run on a background thread! QueueOutputEvent(MessageImportance.Low, buildEvent); IndentOutput(); } /// /// This is the delegate for TargetFinishedHandler events. /// protected virtual void TargetFinishedHandler(object sender, TargetFinishedEventArgs buildEvent) { // NOTE: This may run on a background thread! UnindentOutput(); QueueOutputEvent(MessageImportance.Low, buildEvent); } /// /// This is the delegate for TaskStartedHandler events. /// protected virtual void TaskStartedHandler(object sender, TaskStartedEventArgs buildEvent) { // NOTE: This may run on a background thread! QueueOutputEvent(MessageImportance.Low, buildEvent); IndentOutput(); } /// /// This is the delegate for TaskFinishedHandler events. /// protected virtual void TaskFinishedHandler(object sender, TaskFinishedEventArgs buildEvent) { // NOTE: This may run on a background thread! UnindentOutput(); QueueOutputEvent(MessageImportance.Low, buildEvent); } /// /// This is the delegate for CustomHandler events. /// /// /// protected virtual void CustomHandler(object sender, CustomBuildEventArgs buildEvent) { // NOTE: This may run on a background thread! QueueOutputEvent(MessageImportance.High, buildEvent); } /// /// This is the delegate for error events. /// protected virtual void ErrorHandler(object sender, BuildErrorEventArgs errorEvent) { // NOTE: This may run on a background thread! QueueOutputText(GetFormattedErrorMessage(errorEvent.File, errorEvent.LineNumber, errorEvent.ColumnNumber, false, errorEvent.Code, errorEvent.Message)); QueueTaskEvent(errorEvent); } /// /// This is the delegate for warning events. /// protected virtual void WarningHandler(object sender, BuildWarningEventArgs warningEvent) { // NOTE: This may run on a background thread! QueueOutputText(MessageImportance.High, GetFormattedErrorMessage(warningEvent.File, warningEvent.LineNumber, warningEvent.ColumnNumber, true, warningEvent.Code, warningEvent.Message)); QueueTaskEvent(warningEvent); } /// /// This is the delegate for Message event types /// protected virtual void MessageHandler(object sender, BuildMessageEventArgs messageEvent) { // NOTE: This may run on a background thread! QueueOutputEvent(messageEvent.Importance, messageEvent); } #endregion #region output queue protected void QueueOutputEvent(MessageImportance importance, BuildEventArgs buildEvent) { // NOTE: This may run on a background thread! if (LogAtImportance(importance) && !string.IsNullOrEmpty(buildEvent.Message)) { StringBuilder message = new StringBuilder(this.currentIndent + buildEvent.Message.Length); if (this.currentIndent > 0) { message.Append('\t', this.currentIndent); } message.AppendLine(buildEvent.Message); QueueOutputText(message.ToString()); } } protected void QueueOutputText(MessageImportance importance, string text) { // NOTE: This may run on a background thread! if (LogAtImportance(importance)) { QueueOutputText(text); } } protected void QueueOutputText(string text) { // NOTE: This may run on a background thread! if (this.OutputWindowPane != null) { // Enqueue the output text this.outputQueue.Enqueue(text); // We want to interactively report the output. But we dont want to dispatch // more than one at a time, otherwise we might overflow the main thread's // message queue. So, we only report the output if the queue was empty. if (this.outputQueue.Count == 1) { ReportQueuedOutput(); } } } private void IndentOutput() { // NOTE: This may run on a background thread! this.currentIndent++; } private void UnindentOutput() { // NOTE: This may run on a background thread! this.currentIndent--; } private void ReportQueuedOutput() { // NOTE: This may run on a background thread! // We need to output this on the main thread. We must use BeginInvoke because the main thread may not be pumping events yet. BeginInvokeWithErrorMessage(this.serviceProvider, this.dispatcher, () => { if (this.OutputWindowPane != null) { string outputString; while (this.outputQueue.TryDequeue(out outputString)) { Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(this.OutputWindowPane.OutputString(outputString)); } } }); } private void ClearQueuedOutput() { // NOTE: This may run on a background thread! this.outputQueue = new ConcurrentQueue(); } #endregion output queue #region task queue protected void QueueTaskEvent(BuildEventArgs errorEvent) { this.taskQueue.Enqueue(() => { ErrorTask task = new ErrorTask(); if (errorEvent is BuildErrorEventArgs) { BuildErrorEventArgs errorArgs = (BuildErrorEventArgs)errorEvent; task.Document = errorArgs.File; task.ErrorCategory = TaskErrorCategory.Error; task.Line = errorArgs.LineNumber - 1; // The task list does +1 before showing this number. task.Column = errorArgs.ColumnNumber; task.Priority = TaskPriority.High; } else if (errorEvent is BuildWarningEventArgs) { BuildWarningEventArgs warningArgs = (BuildWarningEventArgs)errorEvent; task.Document = warningArgs.File; task.ErrorCategory = TaskErrorCategory.Warning; task.Line = warningArgs.LineNumber - 1; // The task list does +1 before showing this number. task.Column = warningArgs.ColumnNumber; task.Priority = TaskPriority.Normal; } task.Text = errorEvent.Message; task.Category = TaskCategory.BuildCompile; task.HierarchyItem = hierarchy; return task; }); // NOTE: Unlike output we dont want to interactively report the tasks. So we never queue // call ReportQueuedTasks here. We do this when the build finishes. } private void ReportQueuedTasks() { // NOTE: This may run on a background thread! // We need to output this on the main thread. We must use BeginInvoke because the main thread may not be pumping events yet. BeginInvokeWithErrorMessage(this.serviceProvider, this.dispatcher, () => { this.taskProvider.SuspendRefresh(); try { Func taskFunc; while (this.taskQueue.TryDequeue(out taskFunc)) { // Create the error task ErrorTask task = taskFunc(); // Log the task this.taskProvider.Tasks.Add(task); } } finally { this.taskProvider.ResumeRefresh(); } }); } private void ClearQueuedTasks() { // NOTE: This may run on a background thread! this.taskQueue = new ConcurrentQueue>(); if (this.InteractiveBuild) { // We need to clear this on the main thread. We must use BeginInvoke because the main thread may not be pumping events yet. BeginInvokeWithErrorMessage(this.serviceProvider, this.dispatcher, () => { this.taskProvider.Tasks.Clear(); }); } } #endregion task queue #region helpers /// /// This method takes a MessageImportance and returns true if messages /// at importance i should be loggeed. Otherwise return false. /// private bool LogAtImportance(MessageImportance importance) { // If importance is too low for current settings, ignore the event bool logIt = false; this.SetVerbosity(); switch (this.Verbosity) { case LoggerVerbosity.Quiet: logIt = false; break; case LoggerVerbosity.Minimal: logIt = (importance == MessageImportance.High); break; case LoggerVerbosity.Normal: // Falling through... case LoggerVerbosity.Detailed: logIt = (importance != MessageImportance.Low); break; case LoggerVerbosity.Diagnostic: logIt = true; break; default: Debug.Fail("Unknown Verbosity level. Ignoring will cause everything to be logged"); break; } return logIt; } /// /// Format error messages for the task list /// private string GetFormattedErrorMessage( string fileName, int line, int column, bool isWarning, string errorNumber, string errorText) { string errorCode = isWarning ? this.WarningString : this.ErrorString; StringBuilder message = new StringBuilder(); if (!string.IsNullOrEmpty(fileName)) { message.AppendFormat(CultureInfo.CurrentCulture, "{0}({1},{2}):", fileName, line, column); } message.AppendFormat(CultureInfo.CurrentCulture, " {0} {1}: {2}", errorCode, errorNumber, errorText); message.AppendLine(); return message.ToString(); } /// /// Sets the verbosity level. /// private void SetVerbosity() { // TODO: This should be replaced when we have a version that supports automation. if (!this.haveCachedVerbosity) { string verbosityKey = String.Format(CultureInfo.InvariantCulture, @"{0}\{1}", BuildVerbosityRegistryRoot, buildVerbosityRegistrySubKey); using (RegistryKey subKey = Registry.CurrentUser.OpenSubKey(verbosityKey)) { if (subKey != null) { object valueAsObject = subKey.GetValue(buildVerbosityRegistryKey); if (valueAsObject != null) { this.Verbosity = (LoggerVerbosity)((int)valueAsObject); } } } this.haveCachedVerbosity = true; } } /// /// Clear the cached verbosity, so that it will be re-evaluated from the build verbosity registry key. /// private void ClearCachedVerbosity() { this.haveCachedVerbosity = false; } #endregion helpers #region exception handling helpers /// /// Call Dispatcher.BeginInvoke, showing an error message if there was a non-critical exception. /// /// service provider /// dispatcher /// action to invoke private static void BeginInvokeWithErrorMessage(IServiceProvider serviceProvider, Dispatcher dispatcher, Action action) { dispatcher.BeginInvoke(new Action(() => CallWithErrorMessage(serviceProvider, action))); } /// /// Show error message if exception is caught when invoking a method /// /// service provider /// action to invoke private static void CallWithErrorMessage(IServiceProvider serviceProvider, Action action) { try { action(); } catch (Exception ex) { if (Microsoft.VisualStudio.ErrorHandler.IsCriticalException(ex)) { throw; } ShowErrorMessage(serviceProvider, ex); } } /// /// Show error window about the exception /// /// service provider /// exception private static void ShowErrorMessage(IServiceProvider serviceProvider, Exception exception) { IUIService UIservice = (IUIService)serviceProvider.GetService(typeof(IUIService)); if (UIservice != null && exception != null) { UIservice.ShowError(exception); } } #endregion exception handling helpers } }