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