1 // Copyright (c) Microsoft. All rights reserved.
2 // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3 
4 using System;
5 using System.Collections.Generic;
6 using System.Collections;
7 using System.Text;
8 using Microsoft.Build.Framework;
9 using Microsoft.Build.Shared;
10 using System.IO;
11 using System.Diagnostics;
12 using System.Threading;
13 using System.Globalization;
14 
15 namespace Microsoft.Build.BackEnd.Logging
16 {
17     /// <summary>
18     /// Stores and manages projects and targets events for logging purposes
19     /// </summary>
20     internal class BuildEventManager
21     {
22         #region Data
23         private Dictionary<BuildEventContext, ProjectStartedEventMinimumFields> _projectStartedEvents;
24         private Dictionary<BuildEventContext, TargetStartedEventMinimumFields> _targetStartedEvents;
25         private Dictionary<string, int> _projectTargetKey = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
26         private Dictionary<string, int> _projectKey = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
27         private static ComparerContextNodeId<BuildEventContext> s_compareContextNodeId = new ComparerContextNodeId<BuildEventContext>();
28         private static ComparerContextNodeIdTargetId<BuildEventContext> s_compareContextNodeIdTargetId = new ComparerContextNodeIdTargetId<BuildEventContext>();
29         private int _projectIncrementKey;
30         #endregion
31 
32         #region Constructors
BuildEventManager()33         internal BuildEventManager()
34         {
35             _projectStartedEvents = new Dictionary<BuildEventContext, ProjectStartedEventMinimumFields>(s_compareContextNodeId);
36             _targetStartedEvents = new Dictionary<BuildEventContext, TargetStartedEventMinimumFields>(s_compareContextNodeIdTargetId);
37             _projectIncrementKey = 0;
38         }
39         #endregion
40 
41         #region Methods
42         /// <summary>
43         ///  Adds a new project to the list of project started events which have been fired
44         /// </summary>
AddProjectStartedEvent(ProjectStartedEventArgs e, bool requireTimestamp)45         internal void AddProjectStartedEvent(ProjectStartedEventArgs e, bool requireTimestamp)
46         {   //Parent event can be null if this is the root project
47             ProjectStartedEventMinimumFields parentEvent = GetProjectStartedEvent(e.ParentProjectBuildEventContext);
48             lock (_projectStartedEvents)
49             {
50                 if (!_projectStartedEvents.ContainsKey(e.BuildEventContext))
51                 {
52                     int projectTargetKeyLocal = 1;
53                     int projectIncrementKeyLocal = 1;
54                     // If we haven't seen this project before (by full path) then
55                     // allocate a new key for it and save it away
56                     if (!_projectKey.ContainsKey(e.ProjectFile))
57                     {
58                         _projectIncrementKey += 1;
59 
60                         _projectKey[e.ProjectFile] = _projectIncrementKey;
61                         projectIncrementKeyLocal = _projectIncrementKey;
62                     }
63                     else
64                     {
65                         // We've seen this project before, so retrieve it
66                         projectIncrementKeyLocal = _projectKey[e.ProjectFile];
67                     }
68 
69                     // If we haven't seen any entrypoint for the current project (by full path) then
70                     // allocate a new entry point key
71                     if (!_projectTargetKey.ContainsKey(e.ProjectFile))
72                     {
73                         _projectTargetKey[e.ProjectFile] = projectTargetKeyLocal;
74                     }
75                     else
76                     {
77                         // We've seen this project before, but not this entrypoint, so increment
78                         // the entrypoint key that we have.
79                         projectTargetKeyLocal = _projectTargetKey[e.ProjectFile] + 1;
80                         _projectTargetKey[e.ProjectFile] = projectTargetKeyLocal;
81                     }
82 
83                     _projectStartedEvents.Add(e.BuildEventContext, new ProjectStartedEventMinimumFields(projectIncrementKeyLocal, projectTargetKeyLocal, e, parentEvent, requireTimestamp));
84                 }
85             }
86         }
87 
88         /// <summary>
89         ///  Adds a new target to the list of project started events which have been fired
90         /// </summary>
AddTargetStartedEvent(TargetStartedEventArgs e, bool requireTimeStamp)91         internal void AddTargetStartedEvent(TargetStartedEventArgs e, bool requireTimeStamp)
92         {
93             if (!_targetStartedEvents.ContainsKey(e.BuildEventContext))
94             {
95                 _targetStartedEvents.Add(e.BuildEventContext, new TargetStartedEventMinimumFields(e, requireTimeStamp));
96             }
97         }
98 
99         /// <summary>
100         /// Get a call stack of event contexts for a starting point event context
101         /// </summary>
GetProjectCallStack(BuildEventContext e)102         internal List<ProjectStartedEventMinimumFields> GetProjectCallStack(BuildEventContext e)
103         {
104             List<ProjectStartedEventMinimumFields> stackTrace = new List<ProjectStartedEventMinimumFields>();
105 
106             ProjectStartedEventMinimumFields currentKey = GetProjectStartedEvent(e);
107 
108             // currentKey can be null if the stack trace is requested before the project started event has been seen
109             // or if the call stack is requested by an event which is not associated with a project such as an event
110             // from the engine itself
111             if (currentKey != null)
112             {
113                 //Add the event where the stack should start
114                 stackTrace.Add(currentKey);
115 
116                 // Loop through the call tree until the root project started event has been found
117                 while (currentKey.ParentProjectStartedEvent != null)
118                 {
119                     currentKey = currentKey.ParentProjectStartedEvent;
120                     stackTrace.Add(currentKey);
121                 }
122             }
123             return stackTrace;
124         }
125 
126         /// <summary>
127         /// Set an error flag on all projects in the call stack of a given event context
128         /// </summary>
SetErrorWarningFlagOnCallStack(BuildEventContext e)129         internal void SetErrorWarningFlagOnCallStack(BuildEventContext e)
130         {
131             List<ProjectStartedEventMinimumFields> projectStackTrace = GetProjectCallStack(e);
132             foreach (ProjectStartedEventMinimumFields startedEvent in projectStackTrace)
133             {
134                 // Can be null if the event occures before the project startedEvent or outside of a project
135                 if (startedEvent != null)
136                 {
137                     startedEvent.ErrorInProject = true;
138                 }
139             }
140         }
141 
142         /// <summary>
143         /// Retrieve the project call stack based on the starting point of buildEventContext e
144         /// </summary>
ProjectCallStackFromProject(BuildEventContext e)145         internal string[] ProjectCallStackFromProject(BuildEventContext e)
146         {
147             BuildEventContext currentKey = e;
148 
149             ProjectStartedEventMinimumFields startedEvent = GetProjectStartedEvent(currentKey);
150 
151             List<string> stackTrace = new List<string>();
152             // If there is no started event then there should be no stack trace
153             // this is a valid situation if the event occures in the engine or outside the context of a project
154             // or the event is raised before the project started event
155             if (startedEvent == null)
156             {
157                 return Array.Empty<string>();
158             }
159 
160             List<ProjectStartedEventMinimumFields> projectStackTrace = GetProjectCallStack(e);
161             foreach (ProjectStartedEventMinimumFields projectStartedEvent in projectStackTrace)
162             {
163                 if (!string.IsNullOrEmpty(projectStartedEvent.TargetNames))
164                 {
165                     stackTrace.Add(ResourceUtilities.FormatResourceString("ProjectStackWithTargetNames", projectStartedEvent.ProjectFile, projectStartedEvent.TargetNames, projectStartedEvent.FullProjectKey));
166                 }
167                 else
168                 {
169                     stackTrace.Add(ResourceUtilities.FormatResourceString("ProjectStackWithDefaultTargets", projectStartedEvent.ProjectFile, projectStartedEvent.FullProjectKey));
170                 }
171             }
172             stackTrace.Reverse();
173             return stackTrace.ToArray();
174         }
175 
176         /// <summary>
177         /// Get a deferred project started event based on a given event context
178         /// </summary>
GetProjectStartedEvent(BuildEventContext e)179         internal ProjectStartedEventMinimumFields GetProjectStartedEvent(BuildEventContext e)
180         {
181             ProjectStartedEventMinimumFields buildEvent;
182             if (_projectStartedEvents.ContainsKey(e))
183             {
184                 buildEvent = _projectStartedEvents[e];
185             }
186             else
187             {
188                 buildEvent = null;
189             }
190             return buildEvent;
191         }
192 
193         /// <summary>
194         ///  Get a deferred target started event based on a given event context
195         /// </summary>
GetTargetStartedEvent(BuildEventContext e)196         internal TargetStartedEventMinimumFields GetTargetStartedEvent(BuildEventContext e)
197         {
198             TargetStartedEventMinimumFields buildEvent;
199             if (_targetStartedEvents.ContainsKey(e))
200             {
201                 buildEvent = _targetStartedEvents[e];
202             }
203             else
204             {
205                 buildEvent = null;
206             }
207             return buildEvent;
208         }
209 
210         /// <summary>
211         /// Will remove a project started event from the list of deferred project started events
212         /// </summary>
RemoveProjectStartedEvent(BuildEventContext e)213         internal void RemoveProjectStartedEvent(BuildEventContext e)
214         {
215             ProjectStartedEventMinimumFields startedEvent = GetProjectStartedEvent(e);
216             // Only remove the project from the event list if it is in the list, and no errors have occured in the project
217             if (startedEvent != null && !startedEvent.ErrorInProject)
218             {
219                 _projectStartedEvents.Remove(e);
220             }
221         }
222 
223         /// <summary>
224         /// Will remove a project started event from the list of deferred project started events
225         /// </summary>
RemoveTargetStartedEvent(BuildEventContext e)226         internal void RemoveTargetStartedEvent(BuildEventContext e)
227         {
228             TargetStartedEventMinimumFields startedEvent = GetTargetStartedEvent(e);
229             // Only remove the project from the event list if it is in the list, and no errors have occured in the project
230             if (startedEvent != null && !startedEvent.ErrorInTarget)
231             {
232                 _targetStartedEvents.Remove(e);
233             }
234         }
235         #endregion
236     }
237 
238     /// <summary>
239     /// Compares two event contexts on ProjectContextId and NodeId only
240     /// </summary>
241     internal class ComparerContextNodeId<T> : IEqualityComparer<T>
242     {
243         #region Methods
Equals(T x, T y)244         public bool Equals(T x, T y)
245         {
246             BuildEventContext contextX = x as BuildEventContext;
247             BuildEventContext contextY = y as BuildEventContext;
248 
249             if (contextX == null || contextY == null)
250             {
251                 return false;
252             }
253 
254             // Return true if the fields match:
255             return (contextX.NodeId == contextY.NodeId)
256                    && (contextX.ProjectContextId == contextY.ProjectContextId);
257         }
258 
GetHashCode(T x)259         public int GetHashCode(T x)
260         {
261             BuildEventContext context = x as BuildEventContext;
262             return (context.ProjectContextId + (context.NodeId << 24));
263         }
264         #endregion
265     }
266 
267     /// <summary>
268     /// Compares two event contexts based on the ProjectContextId, NodeId, and TargetId only
269     /// </summary>
270     internal class ComparerContextNodeIdTargetId<T> : IEqualityComparer<T>
271     {
272         #region Methods
Equals(T x, T y)273         public bool Equals(T x, T y)
274         {
275             BuildEventContext contextX = x as BuildEventContext;
276             BuildEventContext contextY = y as BuildEventContext;
277 
278             if (contextX == null || contextY == null)
279             {
280                 return false;
281             }
282 
283             // Return true if the fields match:
284             return (contextX.NodeId == contextY.NodeId)
285                    && (contextX.ProjectContextId == contextY.ProjectContextId)
286                    && (contextX.TargetId == contextY.TargetId);
287         }
288 
GetHashCode(T x)289         public int GetHashCode(T x)
290         {
291             BuildEventContext context = x as BuildEventContext;
292             return (context.ProjectContextId + (context.NodeId << 24));
293         }
294 
295         #endregion
296     }
297 
298     /// <summary>
299     /// This class stands in for a full project started event because it contains only the
300     /// minimum amount of inforomation needed for the logger
301     /// </summary>
302     internal class ProjectStartedEventMinimumFields
303     {
304         #region Data
305         private DateTime _timeStamp;
306         private string _targetNames;
307         private string _projectFile;
308         private bool _showProjectFinishedEvent;
309         private bool _errorInProject;
310         private int _projectId;
311         private ProjectFullKey _projectFullKey;
312         private BuildEventContext _buildEventContext;
313         private ProjectStartedEventMinimumFields _parentProjectStartedEvent;
314         #endregion
315 
316         #region Properties
317 
318         internal DateTime TimeStamp
319         {
320             get
321             {
322                 return _timeStamp;
323             }
324         }
325 
326         internal int ProjectKey
327         {
328             get
329             {
330                 return _projectFullKey.ProjectKey;
331             }
332         }
333 
334         internal int EntryPointKey
335         {
336             get
337             {
338                 return _projectFullKey.EntryPointKey;
339             }
340         }
341 
342         internal string FullProjectKey
343         {
344             get
345             {
346                 return _projectFullKey.ToString();
347             }
348         }
349 
350         internal ProjectStartedEventMinimumFields ParentProjectStartedEvent
351         {
352             get
353             {
354                 return _parentProjectStartedEvent;
355             }
356         }
357 
358         internal string TargetNames
359         {
360             get
361             {
362                 return _targetNames;
363             }
364         }
365 
366         internal int ProjectId
367         {
368             get
369             {
370                 return _projectId;
371             }
372         }
373 
374         internal string ProjectFile
375         {
376             get
377             {
378                 return _projectFile;
379             }
380         }
381 
382         internal bool ShowProjectFinishedEvent
383         {
384             get
385             {
386                 return _showProjectFinishedEvent;
387             }
388 
389             set
390             {
391                 _showProjectFinishedEvent = value;
392             }
393         }
394 
395         internal bool ErrorInProject
396         {
397             get
398             {
399                 return _errorInProject;
400             }
401 
402             set
403             {
404                 _errorInProject = value;
405             }
406         }
407 
408         internal BuildEventContext ProjectBuildEventContext
409         {
410             get
411             {
412                 return _buildEventContext;
413             }
414         }
415         #endregion
416 
417         #region Constructors
ProjectStartedEventMinimumFields(int projectKey, int entryPointKey, ProjectStartedEventArgs startedEvent, ProjectStartedEventMinimumFields parentProjectStartedEvent, bool requireTimeStamp)418         internal ProjectStartedEventMinimumFields(int projectKey, int entryPointKey, ProjectStartedEventArgs startedEvent, ProjectStartedEventMinimumFields parentProjectStartedEvent, bool requireTimeStamp)
419         {
420             _targetNames = startedEvent.TargetNames;
421             _projectFile = startedEvent.ProjectFile;
422             _showProjectFinishedEvent = false;
423             _errorInProject = false;
424             _projectId = startedEvent.ProjectId;
425             _buildEventContext = startedEvent.BuildEventContext;
426             _parentProjectStartedEvent = parentProjectStartedEvent;
427             _projectFullKey = new ProjectFullKey(projectKey, entryPointKey);
428             if (requireTimeStamp)
429             {
430                 _timeStamp = startedEvent.Timestamp;
431             }
432         }
433         #endregion
434     }
435 
436     /// <summary>
437     /// This class stands in for a full target started event because it contains only the
438     /// minimum amount of inforomation needed for the logger
439     /// </summary>
440     internal class TargetStartedEventMinimumFields
441     {
442         #region Data
443         private DateTime _timeStamp;
444         private string _targetName;
445         private string _targetFile;
446         private string _projectFile;
447         private string _parentTarget;
448         private bool _showTargetFinishedEvent;
449         private bool _errorInTarget;
450         private string _message;
451         private BuildEventContext _buildEventContext;
452         #endregion
453 
454         #region Properties
455         internal DateTime TimeStamp
456         {
457             get
458             {
459                 return _timeStamp;
460             }
461         }
462 
463         internal string TargetName
464         {
465             get
466             {
467                 return _targetName;
468             }
469         }
470 
471         internal string TargetFile
472         {
473             get
474             {
475                 return _targetFile;
476             }
477         }
478 
479         internal string ProjectFile
480         {
481             get
482             {
483                 return _projectFile;
484             }
485         }
486 
487         internal string Message
488         {
489             get
490             {
491                 return _message;
492             }
493         }
494 
495         internal bool ShowTargetFinishedEvent
496         {
497             get
498             {
499                 return _showTargetFinishedEvent;
500             }
501 
502             set
503             {
504                 _showTargetFinishedEvent = value;
505             }
506         }
507 
508         internal bool ErrorInTarget
509         {
510             get
511             {
512                 return _errorInTarget;
513             }
514 
515             set
516             {
517                 _errorInTarget = value;
518             }
519         }
520         internal BuildEventContext ProjectBuildEventContext
521         {
522             get
523             {
524                 return _buildEventContext;
525             }
526         }
527 
528         internal string ParentTarget
529         {
530             get
531             {
532                 return _parentTarget;
533             }
534         }
535         #endregion
536 
537         #region Constructors
TargetStartedEventMinimumFields(TargetStartedEventArgs startedEvent, bool requireTimeStamp)538         internal TargetStartedEventMinimumFields(TargetStartedEventArgs startedEvent, bool requireTimeStamp)
539         {
540             _targetName = startedEvent.TargetName;
541             _targetFile = startedEvent.TargetFile;
542             _projectFile = startedEvent.ProjectFile;
543             this.ShowTargetFinishedEvent = false;
544             _errorInTarget = false;
545             _message = startedEvent.Message;
546             _buildEventContext = startedEvent.BuildEventContext;
547             if (requireTimeStamp)
548             {
549                 _timeStamp = startedEvent.Timestamp;
550             }
551             _parentTarget = startedEvent.ParentTarget;
552         }
553         #endregion
554     }
555 
556     /// <summary>
557     /// This class is used as a key to group warnings and errors by the project entry point and the target they
558     /// error or warning was in
559     /// </summary>
560     internal class ErrorWarningSummaryDictionaryKey
561     {
562         #region Data
563         private BuildEventContext _entryPointContext;
564         private string _targetName;
565         private static ComparerContextNodeId<BuildEventContext> s_eventComparer = new ComparerContextNodeId<BuildEventContext>();
566         #endregion
567 
568         #region Constructor
ErrorWarningSummaryDictionaryKey(BuildEventContext entryPoint, string targetName)569         internal ErrorWarningSummaryDictionaryKey(BuildEventContext entryPoint, string targetName)
570         {
571             _entryPointContext = entryPoint;
572             _targetName = targetName == null ? string.Empty : targetName;
573         }
574         #endregion
575 
576         #region Properties
577         internal BuildEventContext EntryPointContext
578         {
579             get
580             {
581                 return _entryPointContext;
582             }
583         }
584 
585         internal string TargetName
586         {
587             get
588             {
589                 return _targetName;
590             }
591         }
592 
593         #endregion
594 
595         #region Equality
596 
Equals(object obj)597         public override bool Equals(object obj)
598         {
599             ErrorWarningSummaryDictionaryKey key = obj as ErrorWarningSummaryDictionaryKey;
600             if (key == null)
601             {
602                 return false;
603             }
604             return s_eventComparer.Equals(_entryPointContext, key.EntryPointContext) && (String.Compare(_targetName, key.TargetName, StringComparison.OrdinalIgnoreCase) == 0);
605         }
606 
GetHashCode()607         public override int GetHashCode()
608         {
609             return (_entryPointContext.GetHashCode() + _targetName.GetHashCode());
610         }
611         #endregion
612 
613     }
614 
615     /// <summary>
616     /// Structure that holds both project and entrypoint keys
617     /// </summary>
618     internal class ProjectFullKey
619     {
620         #region Data
621         private int _projectKey;
622         private int _entryPointKey;
623         #endregion
624 
625         #region Properties
626         internal int ProjectKey
627         {
628             get { return _projectKey; }
629             set { _projectKey = value; }
630         }
631 
632         internal int EntryPointKey
633         {
634             get { return _entryPointKey; }
635             set { _entryPointKey = value; }
636         }
637         #endregion
638 
639         #region Constructor
ProjectFullKey(int projectKey, int entryPointKey)640         internal ProjectFullKey(int projectKey, int entryPointKey)
641         {
642             _projectKey = projectKey;
643             _entryPointKey = entryPointKey;
644         }
645         #endregion
646 
647         #region ToString
648         /// <summary>
649         /// Output the projectKey or the projectKey and the entrypointKey depending on the verbosity level of the logger
650         /// </summary>
651 
ToString(LoggerVerbosity verbosity)652         public string ToString(LoggerVerbosity verbosity)
653         {
654             string fullProjectKey;
655 
656             if (verbosity > LoggerVerbosity.Normal)
657             {
658                 fullProjectKey = this.ToString();
659             }
660             else
661             {
662                 fullProjectKey = String.Format(CultureInfo.InvariantCulture, "{0}", _projectKey);
663             }
664 
665             return fullProjectKey;
666         }
667         /// <summary>
668         /// The default of he ToString method should  be to output the projectKey or the projectKey and the entrypointKey depending if a
669         /// entry point key exists or not
670         /// </summary>
671         /// <returns></returns>
ToString()672         public override string ToString()
673         {
674             string fullProjectKey;
675 
676             if (_entryPointKey > 1)
677             {
678                 fullProjectKey = String.Format(CultureInfo.InvariantCulture, "{0}:{1}", _projectKey, _entryPointKey);
679             }
680             else
681             {
682                 fullProjectKey = String.Format(CultureInfo.InvariantCulture, "{0}", _projectKey);
683             }
684 
685             return fullProjectKey;
686         }
687         #endregion
688 
689         #region Equality
Equals(object obj)690         public override bool Equals(object obj)
691         {
692             ProjectFullKey compareKey = obj as ProjectFullKey;
693             if (compareKey != null)
694             {
695                 return ((compareKey._projectKey == _projectKey) && (compareKey._entryPointKey == _entryPointKey));
696             }
697             else
698             {
699                 return false;
700             }
701         }
702 
GetHashCode()703         public override int GetHashCode()
704         {
705             return (_projectKey + (_entryPointKey << 16));
706         }
707         #endregion
708     }
709 }
710