// Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Threading.Tasks; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.AzureAppServices.FunctionalTests { public class TestCommand { private string _dotnetPath = GetDotnetPath(); private static string GetDotnetPath() { var current = new DirectoryInfo(Directory.GetCurrentDirectory()); while (current != null) { var dotnetSubdir = new DirectoryInfo(Path.Combine(current.FullName, ".test-dotnet")); if (dotnetSubdir.Exists) { var dotnetName = Path.Combine(dotnetSubdir.FullName, "dotnet.exe"); if (!File.Exists(dotnetName)) { throw new InvalidOperationException("dotnet directory was found but dotnet.exe is not in it"); } return dotnetName; } current = current.Parent; } throw new InvalidOperationException("dotnet executable was not found"); } private List _cliGeneratedEnvironmentVariables = new List { "MSBuildSDKsPath" }; protected string _command; public Process CurrentProcess { get; private set; } public Dictionary Environment { get; } = new Dictionary(); public event DataReceivedEventHandler ErrorDataReceived; public event DataReceivedEventHandler OutputDataReceived; public string WorkingDirectory { get; set; } public ILogger Logger { get; set; } public TestCommand(string command) { _command = command; } public void KillTree() { if (CurrentProcess == null) { throw new InvalidOperationException("No process is available to be killed"); } CurrentProcess.KillTree(); } public virtual async Task ExecuteAsync(string args = "") { var resolvedCommand = _command; ResolveCommand(ref resolvedCommand, ref args); Logger.LogInformation($"Executing - {resolvedCommand} {args} - {WorkingDirectoryInfo()}"); return await ExecuteAsyncInternal(resolvedCommand, args); } private async Task ExecuteAsyncInternal(string executable, string args) { var stdOut = new List(); var stdErr = new List(); CurrentProcess = CreateProcess(executable, args); CurrentProcess.ErrorDataReceived += (s, e) => { stdErr.Add(e.Data); var handler = ErrorDataReceived; if (handler != null) { handler(s, e); } }; CurrentProcess.OutputDataReceived += (s, e) => { stdOut.Add(e.Data); var handler = OutputDataReceived; if (handler != null) { handler(s, e); } }; var completionTask = StartAndWaitForExitAsync(CurrentProcess); CurrentProcess.BeginOutputReadLine(); CurrentProcess.BeginErrorReadLine(); await completionTask; CurrentProcess.WaitForExit(); RemoveNullTerminator(stdOut); RemoveNullTerminator(stdErr); var stdOutString = String.Join(System.Environment.NewLine, stdOut); var stdErrString = String.Join(System.Environment.NewLine, stdErr); if (!string.IsNullOrWhiteSpace(stdOutString)) { Logger.LogInformation("stdout: {out}", stdOutString); } if (!string.IsNullOrWhiteSpace(stdErrString)) { Logger.LogInformation("stderr: {err}", stdErrString); } return new CommandResult( CurrentProcess.StartInfo, CurrentProcess.ExitCode, stdOutString, stdErrString); } private Process CreateProcess(string executable, string args) { var psi = new ProcessStartInfo { FileName = executable, Arguments = args, RedirectStandardError = true, RedirectStandardOutput = true, RedirectStandardInput = true, UseShellExecute = false }; RemoveCliGeneratedEnvironmentVariablesFrom(psi); AddEnvironmentVariablesTo(psi); AddWorkingDirectoryTo(psi); var process = new Process { StartInfo = psi }; process.EnableRaisingEvents = true; return process; } private string WorkingDirectoryInfo() { if (WorkingDirectory == null) { return ""; } return $" in {WorkingDirectory}"; } private void RemoveNullTerminator(List strings) { var count = strings.Count; if (count < 1) { return; } if (strings[count - 1] == null) { strings.RemoveAt(count - 1); } } private void ResolveCommand(ref string executable, ref string args) { if (executable == "dotnet") { executable = _dotnetPath; return; } throw new ArgumentOutOfRangeException(nameof(executable)); } private void RemoveCliGeneratedEnvironmentVariablesFrom(ProcessStartInfo psi) { foreach (var name in _cliGeneratedEnvironmentVariables) { psi.Environment.Remove(name); } } private void AddEnvironmentVariablesTo(ProcessStartInfo psi) { foreach (var item in Environment) { psi.Environment[item.Key] = item.Value; } } private void AddWorkingDirectoryTo(ProcessStartInfo psi) { if (!string.IsNullOrWhiteSpace(WorkingDirectory)) { psi.WorkingDirectory = WorkingDirectory; } } public static Task StartAndWaitForExitAsync(Process subject) { var taskCompletionSource = new TaskCompletionSource(); subject.EnableRaisingEvents = true; subject.Exited += (s, a) => { taskCompletionSource.SetResult(null); }; subject.Start(); return taskCompletionSource.Task; } } }