// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml; using Tools.DotNETCommon; namespace AutomationTool { static class ParallelExecutor { [DebuggerDisplay("{Caption}")] class BuildAction { public int SortIndex = -1; public string Caption; public string GroupPrefix; public string OutputPrefix; public string ToolPath; public string ToolArguments; public string WorkingDirectory; public bool bSkipIfProjectFailed; public List Dependants = new List(); public List Dependencies = new List(); public int TotalDependants; public int MissingDependencyCount; public IReadOnlyDictionary Environment; } class BuildActionExecutor { public ManagedProcessGroup ProcessGroup; public BuildAction Action; public List LogLines = new List(); public int ExitCode = -1; private AutoResetEvent CompletedEvent; private List CompletedActions; public BuildActionExecutor(ManagedProcessGroup InProcessGroup, BuildAction InAction, AutoResetEvent InCompletedEvent, List InCompletedActions) { ProcessGroup = InProcessGroup; Action = InAction; CompletedEvent = InCompletedEvent; CompletedActions = InCompletedActions; } public void Run() { if(!String.IsNullOrEmpty(Action.OutputPrefix)) { LogLines.Add(Action.OutputPrefix); } using(ManagedProcess Process = new ManagedProcess(ProcessGroup, Action.ToolPath, Action.ToolArguments, Action.WorkingDirectory, Action.Environment, null, ProcessPriorityClass.BelowNormal)) { LogLines.AddRange(Process.ReadAllLines()); ExitCode = Process.ExitCode; } lock(CompletedActions) { CompletedActions.Add(this); } CompletedEvent.Set(); } } public static int Execute(string ActionsFileName, bool bStopOnErrors) { return Execute(ActionsFileName, UnrealBuildTool.Utils.GetLogicalProcessorCount(), bStopOnErrors); } public static int Execute(string ActionsFileName, int MaxProcesses, bool bStopOnErrors) { List Actions = ReadActions(ActionsFileName); CommandUtils.LogInformation("Building {0} {1} with {2} {3}...", Actions.Count, (Actions.Count == 1) ? "action" : "actions", MaxProcesses, (MaxProcesses == 1)? "process" : "processes"); using (LogIndentScope Indent = new LogIndentScope(" ")) { // Create the list of things to process List QueuedActions = new List(); foreach(BuildAction Action in Actions) { if(Action.MissingDependencyCount == 0) { QueuedActions.Add(Action); } } // Create a job object for all the child processes int ExitCode = 0; string CurrentPrefix = ""; Dictionary ExecutingActions = new Dictionary(); List CompletedActions = new List(); using(ManagedProcessGroup ProcessGroup = new ManagedProcessGroup()) { using(AutoResetEvent CompletedEvent = new AutoResetEvent(false)) { while(QueuedActions.Count > 0 || ExecutingActions.Count > 0) { // Sort the actions by the number of things dependent on them QueuedActions.Sort((A, B) => (A.TotalDependants == B.TotalDependants)? (B.SortIndex - A.SortIndex) : (B.TotalDependants - A.TotalDependants)); // Create threads up to the maximum number of actions while(ExecutingActions.Count < MaxProcesses && QueuedActions.Count > 0) { BuildAction Action = QueuedActions[QueuedActions.Count - 1]; QueuedActions.RemoveAt(QueuedActions.Count - 1); BuildActionExecutor ExecutingAction = new BuildActionExecutor(ProcessGroup, Action, CompletedEvent, CompletedActions); Thread ExecutingThread = new Thread(() => { ExecutingAction.Run(); }); ExecutingThread.Name = String.Format("Build:{0}", Action.Caption); ExecutingThread.Start(); ExecutingActions.Add(ExecutingAction, ExecutingThread); } // Wait for something to finish CompletedEvent.WaitOne(); // Wait for something to finish and flush it to the log lock(CompletedActions) { foreach(BuildActionExecutor CompletedAction in CompletedActions) { // Join the thread Thread CompletedThread = ExecutingActions[CompletedAction]; CompletedThread.Join(); ExecutingActions.Remove(CompletedAction); // Write it to the log if(CompletedAction.LogLines.Count > 0) { if(CurrentPrefix != CompletedAction.Action.GroupPrefix) { CurrentPrefix = CompletedAction.Action.GroupPrefix; CommandUtils.LogInformation(CurrentPrefix); } foreach(string LogLine in CompletedAction.LogLines) { CommandUtils.LogInformation(LogLine); } } // Check the exit code if(CompletedAction.ExitCode == 0) { // Mark all the dependents as done foreach(BuildAction DependantAction in CompletedAction.Action.Dependants) { if(--DependantAction.MissingDependencyCount == 0) { QueuedActions.Add(DependantAction); } } } else { // Update the exit code if it's not already set if(ExitCode == 0) { ExitCode = CompletedAction.ExitCode; } } } CompletedActions.Clear(); } // If we've already got a non-zero exit code, clear out the list of queued actions so nothing else will run if(ExitCode != 0 && bStopOnErrors) { QueuedActions.Clear(); } } } } return ExitCode; } } private static List ReadActions(string InputPath) { XmlDocument File = new XmlDocument(); File.Load(InputPath); XmlNode RootNode = File.FirstChild; if(RootNode == null || RootNode.Name != "BuildSet") { throw new Exception("Incorrect node at root of graph; expected 'BuildSet'"); } XmlNode EnvironmentsNode = RootNode.SelectSingleNode("Environments"); if(EnvironmentsNode == null) { throw new Exception("Missing Environments node under root in XML document"); } // Get the current environment variables Dictionary CurrentEnvironment = new Dictionary(StringComparer.InvariantCultureIgnoreCase); foreach(System.Collections.DictionaryEntry Entry in Environment.GetEnvironmentVariables()) { CurrentEnvironment[Entry.Key.ToString()] = Entry.Value.ToString(); } // Read the tool environment Dictionary NameToTool = new Dictionary(); Dictionary> NameToEnvironment = new Dictionary>(); for(XmlNode EnvironmentNode = EnvironmentsNode.FirstChild; EnvironmentNode != null; EnvironmentNode = EnvironmentNode.NextSibling) { if(EnvironmentNode.Name == "Environment") { // Read the tools nodes XmlNode ToolsNode = EnvironmentNode.SelectSingleNode("Tools"); if(ToolsNode != null) { for(XmlNode ToolNode = ToolsNode.FirstChild; ToolNode != null; ToolNode = ToolNode.NextSibling) { if(ToolNode.Name == "Tool") { XmlNode Name = ToolNode.Attributes.GetNamedItem("Name"); if(Name != null) { NameToTool.Add(Name.Value, ToolNode); } } } } // Read the environment variables for this environment. Each environment has its own set of variables XmlNode VariablesNode = EnvironmentNode.SelectSingleNode("Variables"); if(VariablesNode != null) { XmlNode Name = EnvironmentNode.Attributes.GetNamedItem("Name"); if(Name != null) { Dictionary NamedEnvironment = new Dictionary(CurrentEnvironment, StringComparer.InvariantCultureIgnoreCase); for (XmlNode VariableNode = VariablesNode.FirstChild; VariableNode != null; VariableNode = VariableNode.NextSibling) { if (VariableNode.Name == "Variable") { XmlNode VariableName = VariableNode.Attributes.GetNamedItem("Name"); if(VariableName != null) { NamedEnvironment[VariableName.Value] = VariableNode.Attributes.GetNamedItem("Value").Value; } } } NameToEnvironment[Name.Value] = NamedEnvironment; } } } } // Read all the tasks for each project, and convert them into actions List Actions = new List(); for(XmlNode ProjectNode = RootNode.FirstChild; ProjectNode != null; ProjectNode = ProjectNode.NextSibling) { if(ProjectNode.Name == "Project") { int SortIndex = 0; Dictionary NameToAction = new Dictionary(); for(XmlNode TaskNode = ProjectNode.FirstChild; TaskNode != null; TaskNode = TaskNode.NextSibling) { if(TaskNode.Name == "Task") { XmlNode ToolNode; if(NameToTool.TryGetValue(TaskNode.Attributes.GetNamedItem("Tool").Value, out ToolNode)) { BuildAction Action = FindOrAddAction(NameToAction, TaskNode.Attributes.GetNamedItem("Name").Value, Actions); Action.SortIndex = ++SortIndex; TryGetAttribute(TaskNode, "Caption", out Action.Caption); TryGetAttribute(ToolNode, "GroupPrefix", out Action.GroupPrefix); TryGetAttribute(ToolNode, "OutputPrefix", out Action.OutputPrefix); TryGetAttribute(ToolNode, "Path", out Action.ToolPath); TryGetAttribute(TaskNode, "WorkingDir", out Action.WorkingDirectory); string EnvironmentName; if(!TryGetAttribute(ProjectNode, "Env", out EnvironmentName)) { Action.Environment = CurrentEnvironment; } else if(!NameToEnvironment.TryGetValue(EnvironmentName, out Action.Environment)) { throw new Exception(String.Format("Couldn't find environment '{0}'", EnvironmentName)); } if(TryGetAttribute(ToolNode, "Params", out Action.ToolArguments)) { Action.ToolArguments = ExpandEnvironmentVariables(Action.ToolArguments, Action.Environment); } string SkipIfProjectFailed; if(TryGetAttribute(TaskNode, "SkipIfProjectFailed", out SkipIfProjectFailed) && SkipIfProjectFailed == "True") { Action.bSkipIfProjectFailed = true; } string DependsOnList; if(TryGetAttribute(TaskNode, "DependsOn", out DependsOnList)) { foreach (string DependsOn in DependsOnList.Split(';')) { BuildAction DependsOnAction = FindOrAddAction(NameToAction, DependsOn, Actions); if(!Action.Dependencies.Contains(DependsOnAction)) { Action.Dependencies.Add(DependsOnAction); } if(!DependsOnAction.Dependants.Contains(Action)) { DependsOnAction.Dependants.Add(Action); } } } HashSet VisitedActions = new HashSet(); RecursiveIncDependents(Action, VisitedActions); Action.MissingDependencyCount = Action.Dependencies.Count; } } } // Make sure there have been no actions added which were referenced but never declared foreach(KeyValuePair Pair in NameToAction) { if(Pair.Value.SortIndex == -1) { throw new Exception(String.Format("Action {0} was referenced but never declared", Pair.Key)); } } } } return Actions; } static bool TryGetAttribute(XmlNode Node, string Name, out string Value) { XmlNode Attribute = Node.Attributes.GetNamedItem(Name); if(Attribute == null) { Value = null; return false; } else { Value = Attribute.Value; return true; } } static string ExpandEnvironmentVariables(string Input, IReadOnlyDictionary Environment) { string Output = Input; for(int StartIndex = 0;;) { int Index = Output.IndexOf("$(", StartIndex); if(Index == -1) break; int EndIndex = Output.IndexOf(")", Index); if(EndIndex == -1) break; string VariableName = Output.Substring(Index + 2, EndIndex - Index - 2); string VariableValue; if(Environment.TryGetValue(VariableName, out VariableValue)) { Output = Output.Substring(0, Index) + VariableValue + Output.Substring(EndIndex + 1); StartIndex = Index + VariableValue.Length; } else { CommandUtils.LogWarning("Unknown environment variable '{0}' in script", VariableName); StartIndex = EndIndex + 1; } } return Output; } private static BuildAction FindOrAddAction(Dictionary NameToAction, string Name, List Actions) { BuildAction Action; if(!NameToAction.TryGetValue(Name, out Action)) { Action = new BuildAction(); Actions.Add(Action); NameToAction.Add(Name, Action); } return Action; } private static void RecursiveIncDependents(BuildAction Action, HashSet VisitedActions) { foreach(BuildAction Dependency in Action.Dependencies) { if(!VisitedActions.Contains(Action)) { VisitedActions.Add(Action); Dependency.TotalDependants++; RecursiveIncDependents(Dependency, VisitedActions); } } } } }