/** * The actual function to run in a thread. This is potentially long and blocking */ private void ThreadFunc() { // thread start time Action.StartTime = DateTimeOffset.Now; if (Action.ActionHandler != null) { // call the function and get the ExitCode and an output string string Output; Action.ActionHandler(Action, out ExitCode, out Output); // Output status description (file name) when no output is returned if (string.IsNullOrEmpty(Output)) { if (Action.bShouldOutputStatusDescription) { Output = string.Format("[{0}/{1}] {2} {3}", JobNumber, TotalJobs, Action.CommandDescription, Action.StatusDescription); } else { Output = Action.StatusDescription; } } SendOutputToEventHandler(Action, Output); } else { // batch files (.bat, .cmd) need to be run via or cmd /c or shellexecute, // the latter which we can't use because we want to redirect input/output bool bLaunchViaCmdExe = !Utils.IsRunningOnMono && !Path.GetExtension(Action.CommandPath).ToLower().EndsWith("exe"); // Create the action's process. ProcessStartInfo ActionStartInfo = new ProcessStartInfo(); ActionStartInfo.WorkingDirectory = ExpandEnvironmentVariables(Action.WorkingDirectory); string ExpandedCommandPath = ExpandEnvironmentVariables(Action.CommandPath); if (bLaunchViaCmdExe) { ActionStartInfo.FileName = "cmd.exe"; ActionStartInfo.Arguments = string.Format ( "/c \"{0} {1}\"", ExpandEnvironmentVariables(ExpandedCommandPath), ExpandEnvironmentVariables(Action.CommandArguments) ); } else { ActionStartInfo.FileName = ExpandedCommandPath; ActionStartInfo.Arguments = ExpandEnvironmentVariables(Action.CommandArguments); } ActionStartInfo.UseShellExecute = false; ActionStartInfo.RedirectStandardInput = false; ActionStartInfo.RedirectStandardOutput = false; ActionStartInfo.RedirectStandardError = false; // Log command-line used to execute task if debug info printing is enabled. if (BuildConfiguration.bPrintDebugInfo) { Log.TraceVerbose("Executing: {0} {1}", ExpandedCommandPath, ActionStartInfo.Arguments); } if (Action.bPrintDebugInfo) { Log.TraceInformation("Executing: {0} {1}", ExpandedCommandPath, ActionStartInfo.Arguments); } // Log summary if wanted. else if (Action.bShouldOutputStatusDescription) { string CommandDescription = Action.CommandDescription != null ? Action.CommandDescription : Path.GetFileName(ExpandedCommandPath); if (string.IsNullOrEmpty(CommandDescription)) { Log.TraceInformation(Action.StatusDescription); } else { Log.TraceInformation("[{0}/{1}] {2} {3}", JobNumber, TotalJobs, CommandDescription, Action.StatusDescription); } } // Try to launch the action's process, and produce a friendly error message if it fails. Process ActionProcess = null; try { try { ActionProcess = new Process(); ActionProcess.StartInfo = ActionStartInfo; bool bShouldRedirectOuput = Action.OutputEventHandler != null || Action.bPrintDebugInfo; if (bShouldRedirectOuput) { ActionStartInfo.RedirectStandardOutput = true; ActionStartInfo.RedirectStandardError = true; ActionProcess.EnableRaisingEvents = true; if (Action.OutputEventHandler != null) { ActionProcess.OutputDataReceived += Action.OutputEventHandler; ActionProcess.ErrorDataReceived += Action.OutputEventHandler; } if (Action.bPrintDebugInfo) { ActionProcess.OutputDataReceived += new DataReceivedEventHandler(ActionDebugOutput); ActionProcess.ErrorDataReceived += new DataReceivedEventHandler(ActionDebugOutput); } } ActionProcess.Start(); if (bShouldRedirectOuput) { ActionProcess.BeginOutputReadLine(); ActionProcess.BeginErrorReadLine(); } } catch (Exception ex) { throw new BuildException(ex, "Failed to start local process for action: {0} {1}\r\n{2}", Action.CommandPath, Action.CommandArguments, ex.ToString()); } // wait for process to start // NOTE: this may or may not be necessary; seems to depend on whether the system UBT is running on start the process in a timely manner. int checkIterations = 0; bool haveConfiguredProcess = false; do { if (ActionProcess.HasExited) { if (haveConfiguredProcess == false) { Debug.WriteLine("Process for action exited before able to configure!"); } break; } Thread.Sleep(100); if (!haveConfiguredProcess) { try { ActionProcess.PriorityClass = ProcessPriorityClass.BelowNormal; haveConfiguredProcess = true; } catch (Exception) { } break; } checkIterations++; } while(checkIterations < 10); if (checkIterations == 10) { throw new BuildException("Failed to configure local process for action: {0} {1}", Action.CommandPath, Action.CommandArguments); } // block until it's complete // @todo iosmerge: UBT had started looking at: if (Utils.IsValidProcess(Process)) // do we need to check that in the thread model? while (!ActionProcess.HasExited) { Thread.Sleep(10); } // capture exit code ExitCode = ActionProcess.ExitCode; } finally { // As the process has finished now, free its resources. On non-Windows platforms, processes depend // on POSIX/BSD threading and these are limited per application. Disposing the Process releases // these thread resources. if (ActionProcess != null) { ActionProcess.Close(); } } } // track how long it took Action.EndTime = DateTimeOffset.Now; // send telemetry { // See if the action produced a PCH file (infer from the extension as we no longer have the compile environments used to generate the actions. var ActionProducedPCH = Action.ProducedItems.Find(fileItem => new[] { ".PCH", ".GCH" }.Contains(Path.GetExtension(fileItem.AbsolutePath).ToUpperInvariant())); if (ActionProducedPCH != null && File.Exists(ActionProducedPCH.AbsolutePath)) // File may not exist if we're building remotely { // If we had a valid match for a PCH item, send an event. Telemetry.SendEvent("PCHTime.2", "ExecutorType", "Local", // Use status description because on VC tool chains it tells us more details about shared PCHs and their source modules. "Filename", Action.StatusDescription, // Get the length from the OS instead of the FileItem.Length is really for when the file is used as an input, // so the stored length is absent for a new for or out of date at best. "FileSize", new FileInfo(ActionProducedPCH.AbsolutePath).Length.ToString(), "Duration", Action.Duration.TotalSeconds.ToString("0.00")); } } if (!Utils.IsRunningOnMono) { // let RPCUtilHelper clean up anything thread related RPCUtilHelper.OnThreadComplete(); } // we are done!! bComplete = true; }
public static ExecutionResult ExecuteActions(List <Action> Actions) { ExecutionResult XGEResult = ExecutionResult.TasksSucceeded; if (Actions.Count > 0) { // Write the actions to execute to a XGE task file. string XGETaskFilePath = Path.Combine(BuildConfiguration.BaseIntermediatePath, "XGETasks.xml"); WriteTaskFile(Actions, XGETaskFilePath, ProgressWriter.bWriteMarkup); if (BuildConfiguration.bXGEExport) { SaveXGEFile(XGETaskFilePath); } else { // Try to execute the XGE tasks, and if XGE is available, skip the local execution fallback. if (Telemetry.IsAvailable()) { // Add a custom output handler to determine the build duration for each task and map it back to the action that generated it. XGEResult = XGE.ExecuteTaskFileWithProgressMarkup(XGETaskFilePath, Actions.Count, (sender, args) => { // sometimes the args comes in as null var match = XGEDurationRegex.Match(args.Data ?? ""); // This is considered fragile and risky parsing of undocumented XGE output, so protect this code from taking down UBT try { // Use LINQ to evaluate the terms so we don't end up with a // huge nested conditional expression just to verify the input. foreach (var PCHEvent in // first, convert the input line into an enumerable from RegexMatch in new[] { match } where RegexMatch.Success let Filename = RegexMatch.Groups["Filename"].Value // find the mapped action. (On PS4 at least, XGE appears to use the full filenane, so we need to strip it back down. let Action = Actions.Find(a => a.StatusDescription == Path.GetFileName(Filename) && a.ActionType == ActionType.Compile) // We should ALWAYS find the action, so maybe this should throw if we can't where Action != null // see if the mapped action produces a PCH file // (there is currently no compile environment left around by which to tell absolutely, so we infer by the extension. Ugh). let ActionProducedPCH = Action.ProducedItems.Find(fileItem => new[] { ".PCH", ".GCH" }.Contains(Path.GetExtension(fileItem.AbsolutePath).ToUpperInvariant())) where ActionProducedPCH != null // we found a valid PCH action and output file, so parse the duration and send an event. let durationMatchStr = RegexMatch.Groups["Duration"].Value // if there's no hour designator, add one so .NET can parse the time. let durationStr = durationMatchStr.Count(c => c == ':') == 1 ? "0:" + durationMatchStr : durationMatchStr let duration = TimeSpan.Parse(durationStr) select new { // actually use the filename here because it is coerced to be PCH.MODULE.HEADER.cpp. // This allows us an easy way to determine shared status (or source module) and the real header source. FileName = Filename, //Path.GetFileName(ActionProducedPCH.AbsolutePath), // Get the length from the OS as the FileItem.Length is really for when the file is used as an input, // so the stored length is absent for a new for or out of date at best. GeneratedFileLength = new FileInfo(ActionProducedPCH.AbsolutePath).Length, GeneratedFileDuration = duration, }) { // If we had a valid match for a PCH item, send an event. Telemetry.SendEvent("PCHTime.2", "ExecutorType", "XGE", "Filename", PCHEvent.FileName, "FileSize", PCHEvent.GeneratedFileLength.ToString(), "Duration", PCHEvent.GeneratedFileDuration.TotalSeconds.ToString("0.00")); } } catch (Exception ex) { // report that something went wrong so we can diagnose. Telemetry.SendEvent("PCHTimeError.2", "OutputLine", args.Data, "Exception", ex.ToString()); } // XGE outputs the duration info in a format that makes VC think it's an error file/line notation if the full filename is used. // So munge it a bit so we don't confuse VC. var updatedData = match.Success ? args.Data.Replace('(', '[').Replace(')', ']') : args.Data; // forward the output on to the normal handler or the console like normal if (Actions[0].OutputEventHandler != null) { DataReceivedEventArgs EventArgs = ConstructDataReceivedEventArgs(updatedData); Actions[0].OutputEventHandler(sender, EventArgs); } else { Console.WriteLine(updatedData); } }); } else { XGEResult = XGE.ExecuteTaskFileWithProgressMarkup(XGETaskFilePath, Actions.Count, (Sender, Args) => { if (Actions[0].OutputEventHandler != null) { Actions[0].OutputEventHandler(Sender, Args); } else { Console.WriteLine(Args.Data); } }); } } } return(XGEResult); }