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;
6 using System.Collections.Concurrent;
7 using System.Linq;
8 using System.Xml.Linq;
9 using Microsoft.Build.Framework;
10 
11 namespace Microsoft.Build.Logging.StructuredLogger
12 {
13     /// <summary>
14     /// Class representation of an MSBuild project execution.
15     /// </summary>
16     internal class Project : LogProcessNode
17     {
18         /// <summary>
19         /// The full path to the MSBuild project file for this project.
20         /// </summary>
21         private string _projectFile;
22 
23         /// <summary>
24         /// A lookup table mapping of target names to targets.
25         /// Target names are unique to a project and the id is not always specified in the log.
26         /// </summary>
27         private readonly ConcurrentDictionary<string, Target> _targetNameToTargetMap = new ConcurrentDictionary<string, Target>(StringComparer.OrdinalIgnoreCase);
28 
29         /// <summary>
30         /// Initializes a new instance of the <see cref="Project"/> class.
31         /// </summary>
32         /// <param name="projectId">The project identifier.</param>
33         /// <param name="e">The <see cref="ProjectStartedEventArgs"/> instance containing the event data.</param>
34         /// <param name="parentPropertyBag">The parent property bag (to check for inherited properties).</param>
Project(int projectId, ProjectStartedEventArgs e, PropertyBag parentPropertyBag)35         public Project(int projectId, ProjectStartedEventArgs e, PropertyBag parentPropertyBag)
36         {
37             Properties = new PropertyBag(parentPropertyBag);
38             Id = projectId;
39 
40             TryUpdate(e);
41         }
42 
43         /// <summary>
44         /// Add the given project as a child to this node.
45         /// </summary>
46         /// <param name="childProject">The child project to add.</param>
AddChildProject(Project childProject)47         public void AddChildProject(Project childProject)
48         {
49             AddChildNode(childProject);
50         }
51 
52         /// <summary>
53         /// Adds a new target node to the project.
54         /// </summary>
55         /// <param name="targetStartedEventArgs">The <see cref="TargetStartedEventArgs"/> instance containing the event data.</param>
AddTarget(TargetStartedEventArgs targetStartedEventArgs)56         public void AddTarget(TargetStartedEventArgs targetStartedEventArgs)
57         {
58             var target = GetOrAddTargetByName(targetStartedEventArgs.TargetName, targetStartedEventArgs);
59 
60             if (!string.IsNullOrEmpty(targetStartedEventArgs.ParentTarget))
61             {
62                 var parentTarget = GetOrAddTargetByName(targetStartedEventArgs.ParentTarget);
63                 parentTarget.AddChildTarget(target);
64             }
65             else
66             {
67                 AddChildNode(target);
68             }
69         }
70 
71         /// <summary>
72         /// Gets the child target by identifier.
73         /// </summary>
74         /// <remarks>Throws if the child target does not exist</remarks>
75         /// <param name="id">The target identifier.</param>
76         /// <returns>Target with the given ID</returns>
GetTargetById(int id)77         public Target GetTargetById(int id)
78         {
79             return _targetNameToTargetMap.Values.First(t => t.Id == id);
80         }
81 
82         /// <summary>
83         /// Writes the project and its children to XML XElement representation.
84         /// </summary>
85         /// <param name="parentElement">The parent element.</param>
SaveToElement(XElement parentElement)86         public override void SaveToElement(XElement parentElement)
87         {
88             // We could be in a situation where we never saw a "Parent" Target. So it's now
89             // in our scope but not rooted. This can happen when targets fail to run.
90             // Let's just add them back.
91             foreach (var orphan in _targetNameToTargetMap.Values.Where(t => t.Id < 0))
92             {
93                 AddChildNode(orphan);
94             }
95 
96             var element = new XElement("Project",
97                 new XAttribute("Name", Name.Replace("\"", string.Empty)),
98                 new XAttribute("StartTime", StartTime),
99                 new XAttribute("EndTime", EndTime),
100                 new XAttribute("ProjectFile", _projectFile));
101 
102             parentElement.Add(element);
103 
104             WriteProperties(element);
105             WriteChildren<Message>(element, () => new XElement("ProjectMessageEvents"));
106             WriteChildren<Project>(element);
107             WriteChildren<Target>(element);
108         }
109 
110         /// <summary>
111         /// Try to update the project data given a project started event. This is useful if the project
112         /// was created (e.g. as a parent) before we saw the started event.
113         /// <remarks>Does nothing if the data has already been set or the new data is null.</remarks>
114         /// </summary>
115         /// <param name="projectStartedEventArgs">The <see cref="ProjectStartedEventArgs"/> instance containing the event data.</param>
TryUpdate(ProjectStartedEventArgs projectStartedEventArgs)116         public void TryUpdate(ProjectStartedEventArgs projectStartedEventArgs)
117         {
118             if (Name == null && projectStartedEventArgs != null)
119             {
120                 StartTime = projectStartedEventArgs.Timestamp;
121                 Name = projectStartedEventArgs.Message;
122                 _projectFile = projectStartedEventArgs.ProjectFile;
123 
124                 if (projectStartedEventArgs.GlobalProperties != null)
125                 {
126                     Properties.AddProperties(projectStartedEventArgs.GlobalProperties);
127                 }
128                 if (projectStartedEventArgs.Properties != null)
129                 {
130                     Properties.AddProperties(projectStartedEventArgs.Properties.Cast<DictionaryEntry>());
131                 }
132             }
133         }
134 
135         /// <summary>
136         /// Returns a <see cref="System.String" /> that represents this instance.
137         /// </summary>
138         /// <returns>
139         /// A <see cref="System.String" /> that represents this instance.
140         /// </returns>
ToString()141         public override string ToString()
142         {
143             return string.Format("{0} - {1}", Id, Name);
144         }
145 
146         /// <summary>
147         /// Gets a child target by name. If the target doesn't exist a stub will be created.
148         /// </summary>
149         /// <param name="targetName">Name of the target.</param>
150         /// <param name="e">The <see cref="TargetStartedEventArgs"/> instance containing the event data, if any.</param>
151         /// <returns>Target node</returns>
GetOrAddTargetByName(string targetName, TargetStartedEventArgs e = null)152         private Target GetOrAddTargetByName(string targetName, TargetStartedEventArgs e = null)
153         {
154             Target result = _targetNameToTargetMap.GetOrAdd(targetName, key=> new Target(key, e));
155 
156             if (e != null)
157             {
158                 result.TryUpdate(e);
159             }
160 
161             return result;
162         }
163     }
164 }
165