// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Diagnostics; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests { internal static class MSBuildProcessManager { public static Task RunProcessAsync( ProjectDirectory project, string arguments, TimeSpan? timeout = null, MSBuildProcessKind msBuildProcessKind = MSBuildProcessKind.Dotnet) { timeout = timeout ?? TimeSpan.FromSeconds(120); var processStartInfo = new ProcessStartInfo() { WorkingDirectory = project.DirectoryPath, UseShellExecute = false, RedirectStandardError = true, RedirectStandardOutput = true, }; if (msBuildProcessKind == MSBuildProcessKind.Desktop) { if (string.IsNullOrEmpty(BuildVariables.MSBuildPath)) { throw new ArgumentException("Unable to locate MSBuild.exe to run desktop tests. " + "MSBuild.exe is located using state created as part of running build[cmd|sh] at the root of the repository. Run build /t:Prepeare to set this up if this hasn't been done."); } processStartInfo.FileName = BuildVariables.MSBuildPath; processStartInfo.Arguments = arguments; } else { processStartInfo.FileName = "dotnet"; processStartInfo.Arguments = $"msbuild {arguments}"; } var process = new Process() { StartInfo = processStartInfo, EnableRaisingEvents = true, }; var output = new StringBuilder(); process.ErrorDataReceived += Process_ErrorDataReceived; process.OutputDataReceived += Process_OutputDataReceived; process.Start(); process.BeginErrorReadLine(); process.BeginOutputReadLine(); var timeoutTask = Task.Delay(timeout.Value).ContinueWith((t) => { // Don't timeout during debug sessions while (Debugger.IsAttached) { Thread.Sleep(TimeSpan.FromSeconds(1)); } if (process.HasExited) { // This will happen on success, the 'real' task has already completed so this value will // never be visible. return (MSBuildResult)null; } // This is a timeout. process.Kill(); throw new TimeoutException($"command '${process.StartInfo.FileName} {process.StartInfo.Arguments}' timed out after {timeout}. Output: {output.ToString()}"); }); var waitTask = Task.Run(() => { // We need to use two WaitForExit calls to ensure that all of the output/events are processed. Previously // this code used Process.Exited, which could result in us missing some output due to the ordering of // events. // // See the remarks here: https://msdn.microsoft.com/en-us/library/ty0d8k56(v=vs.110).aspx if (!process.WaitForExit(Int32.MaxValue)) { // unreachable - the timeoutTask will kill the process before this happens. throw new TimeoutException(); } process.WaitForExit(); var result = new MSBuildResult(project, process.StartInfo.FileName, process.StartInfo.Arguments, process.ExitCode, output.ToString()); return result; }); return Task.WhenAny(waitTask, timeoutTask).Unwrap(); void Process_ErrorDataReceived(object sender, DataReceivedEventArgs e) { output.AppendLine(e.Data); } void Process_OutputDataReceived(object sender, DataReceivedEventArgs e) { output.AppendLine(e.Data); } } } }