|
// 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.
#nullable disable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
#if !IS_CORECLR
using System.Reflection;
#endif
using System.Threading;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using NuGet.Common;
namespace NuGet.Build.Tasks
{
/// <summary>
/// Represents a base class for tasks that use the out-of-proc NuGet.Build.Tasks.Console to perform restore operations with MSBuild's static graph.
/// </summary>
public abstract class StaticGraphRestoreTaskBase : Microsoft.Build.Utilities.Task, ICancelableTask, IDisposable
{
internal readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
private readonly IEnvironmentVariableReader _environmentVariableReader;
protected StaticGraphRestoreTaskBase(IEnvironmentVariableReader environmentVariableReader)
: base(Strings.ResourceManager)
{
_environmentVariableReader = environmentVariableReader ?? throw new ArgumentNullException(nameof(environmentVariableReader));
}
/// <summary>
/// Gets the full path to this assembly.
/// </summary>
protected static readonly Lazy<FileInfo> ThisAssemblyLazy = new Lazy<FileInfo>(() => new FileInfo(typeof(RestoreTaskEx).Assembly.Location));
[Output]
public ITaskItem[] EmbedInBinlog { get; set; }
/// <summary>
/// Gets a value indicating whether or not <see cref="SolutionPath" /> contains a value.
/// </summary>
public bool IsSolutionPathDefined => !string.IsNullOrWhiteSpace(SolutionPath) && !string.Equals(SolutionPath, "*Undefined*", StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Gets or sets the full path to the directory containing MSBuild.
/// </summary>
[Required]
public string MSBuildBinPath { get; set; }
/// <summary>
/// MSBuildStartupDirectory - Used to calculate relative paths
/// </summary>
public string MSBuildStartupDirectory { get; set; }
/// <summary>
/// The path to the file to start the additional process with.
/// </summary>
public string ProcessFileName { get; set; }
/// <summary>
/// Gets or sets the full path to the current project file.
/// </summary>
[Required]
public string ProjectFullPath { get; set; }
/// <summary>
/// Get or sets a value indicating whether or not the restore should restore all projects or just the entry project.
/// </summary>
public bool Recursive { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not the global properties should be sent to NuGet.Build.Tasks.Console.exe over the standard input stream.
/// </summary>
public bool SerializeGlobalProperties { get; set; }
/// <summary>
/// Gets or sets the full path to the solution file (if any) that is being built.
/// </summary>
public string SolutionPath { get; set; }
protected abstract string DebugEnvironmentVariableName { get; }
/// <inheritdoc cref="ICancelableTask.Cancel" />
public void Cancel() => _cancellationTokenSource.Cancel();
/// <inheritdoc cref="IDisposable.Dispose" />
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
public override bool Execute()
{
try
{
string debugEnvironmentVariable = _environmentVariableReader.GetEnvironmentVariable(DebugEnvironmentVariableName);
if (string.Equals(debugEnvironmentVariable, bool.TrueString, StringComparison.OrdinalIgnoreCase) ||
string.Equals(debugEnvironmentVariable, "1", StringComparison.OrdinalIgnoreCase))
{
Debugger.Launch();
}
MSBuildLogger logger = new MSBuildLogger(Log);
Dictionary<string, string> globalProperties = GetGlobalProperties();
Encoding utf8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
using (var semaphore = new SemaphoreSlim(initialCount: 0, maxCount: 1))
using (var loggingQueue = new TaskLoggingQueue(Log))
using (var process = new Process())
{
bool errorLogged = false;
process.EnableRaisingEvents = true;
process.StartInfo = new ProcessStartInfo
{
Arguments = GetCommandLineArguments(globalProperties),
CreateNoWindow = true,
FileName = GetProcessFileName(ProcessFileName),
RedirectStandardError = true,
RedirectStandardInput = true,
RedirectStandardOutput = true,
#if !NETFRAMEWORK
StandardInputEncoding = utf8Encoding,
#endif
UseShellExecute = false,
WorkingDirectory = Environment.CurrentDirectory,
};
// Place the output in the queue which handles logging messages coming through on StdOut
process.OutputDataReceived += (sender, args) => loggingQueue.Enqueue(args?.Data);
process.ErrorDataReceived += (_, args) =>
{
if (args.Data != null)
{
// Any message on the standard error stream should be logged as an error
Log.LogError(args.Data);
errorLogged = true;
}
};
process.Exited += (sender, args) => semaphore.Release();
Log.LogMessageFromResources(MessageImportance.Low, nameof(Strings.Log_RunningStaticGraphRestoreCommand), process.StartInfo.FileName, process.StartInfo.Arguments);
Encoding previousConsoleInputEncoding = Console.InputEncoding;
// Set the input encoding to UTF8 without a byte order mark, the spawned process will use this encoding on .NET Framework
Console.InputEncoding = utf8Encoding;
try
{
process.Start();
}
catch (Exception e)
{
Log.LogErrorFromResources(nameof(Strings.Error_StaticGraphRestoreFailedToStart), e.Message);
return false;
}
finally
{
Console.InputEncoding = previousConsoleInputEncoding;
}
process.BeginOutputReadLine();
process.BeginErrorReadLine();
if (SerializeGlobalProperties)
{
using var writer = new BinaryWriter(process.StandardInput.BaseStream, utf8Encoding, leaveOpen: true);
WriteGlobalProperties(writer, globalProperties);
}
process.StandardInput.Close();
try
{
semaphore.Wait(_cancellationTokenSource.Token);
}
catch (Exception e) when (
e is OperationCanceledException
|| (e is AggregateException aggregateException && aggregateException.InnerException is OperationCanceledException))
{
// Wait() throws when the cancellation token is triggered, this is expected so ignore the exception
}
if (!process.HasExited)
{
try
{
// Kill the process in the case of cancellation
process.Kill();
}
catch (InvalidOperationException)
{
// The process may have exited, in this case ignore the exception
}
}
if (_cancellationTokenSource.IsCancellationRequested)
{
// Return true so the task "succeeds" in the case of cancellation so the build doesn't appear to fail
return true;
}
if (process.ExitCode > 0 && !Log.HasLoggedErrors && !errorLogged)
{
// All non-zero exit codes should have logged an error, if not its unexpected so log an error asking the user to file an issue
Log.LogErrorFromResources(nameof(Strings.Error_StaticGraphNonZeroExitCode), process.ExitCode);
}
EmbedInBinlog = loggingQueue.FilesToEmbedInBinlog.Select(i => new TaskItem(i)).ToArray();
}
}
catch (Exception e)
{
Log.LogErrorFromException(e);
}
return !Log.HasLoggedErrors;
}
/// <summary>
/// Gets the command-line arguments to use when launching the process that executes the restore.
/// </summary>
/// <param name="globalProperties">Receives a <see cref="Dictionary{TKey, TValue}" /> containing the global properties.</param>
internal string GetCommandLineArguments(Dictionary<string, string> globalProperties)
{
// First get the command-line arguments including the global properties
return CreateArgumentString(EnumerateCommandLineArguments(SerializeGlobalProperties ? null : globalProperties));
IEnumerable<string> EnumerateCommandLineArguments(Dictionary<string, string> globalProperties)
{
#if IS_CORECLR
// The full path to the executable for dotnet core
yield return Path.Combine(ThisAssemblyLazy.Value.DirectoryName, Path.ChangeExtension(ThisAssemblyLazy.Value.Name, ".Console.dll"));
#endif
var options = GetOptions();
// Semicolon delimited list of options
yield return string.Join(";", options.Select(i => $"{i.Key}={i.Value}"));
// Full path to MSBuild.exe or MSBuild.dll
#if IS_CORECLR
yield return Path.Combine(MSBuildBinPath, "MSBuild.dll");
#else
yield return Path.Combine(MSBuildBinPath, "MSBuild.exe");
#endif
// Full path to the entry project. If its a solution file, it will be the full path to solution, otherwise SolutionPath is either empty
// or is the value "*Undefined*" and ProjectFullPath is set instead.
yield return IsSolutionPathDefined
? SolutionPath
: ProjectFullPath;
// globalProperties is null when they will be serialized over the standard input stream instead
if (globalProperties != null)
{
// Semicolon delimited list of MSBuild global properties
yield return string.Join(";", globalProperties.Select(i => $"{i.Key}={i.Value}"));
}
}
}
/// <summary>
/// Creates an argument string of space delimited quoted arguments.
/// </summary>
/// <param name="arguments">An <see cref="IEnumerable{T}" /> containing individual argument values.</param>
/// <returns>A <see cref="string" /> with space delimited quoted arguments.</returns>
internal static string CreateArgumentString(IEnumerable<string> arguments)
{
return "\"" + string.Join("\" \"", arguments) + "\"";
}
/// <summary>
/// Enumerates a list of global properties for the current MSBuild instance.
/// </summary>
/// <returns>A <see cref="Dictionary{TKey, TValue}" /> containing global properties.</returns>
internal virtual Dictionary<string, string> GetGlobalProperties()
{
IReadOnlyDictionary<string, string> globalProperties = null;
#if IS_CORECLR
// MSBuild 16.5 and above has a method to get the global properties, older versions do not
if (BuildEngine is IBuildEngine6 buildEngine6)
{
globalProperties = buildEngine6.GetGlobalProperties();
}
#else
// MSBuild 16.5 added a new interface, IBuildEngine6, which has a GetGlobalProperties() method. However, we compile against
// Microsoft.Build.Framework version 4.0 when targeting .NET Framework, so reflection is required since type checking
// can't be done at compile time
Type buildEngine6Type = typeof(IBuildEngine).Assembly.GetType("Microsoft.Build.Framework.IBuildEngine6");
if (buildEngine6Type != null)
{
MethodInfo getGlobalPropertiesMethod = buildEngine6Type.GetMethod("GetGlobalProperties", BindingFlags.Instance | BindingFlags.Public);
if (getGlobalPropertiesMethod != null)
{
try
{
globalProperties = getGlobalPropertiesMethod.Invoke(BuildEngine, parameters: null) as IReadOnlyDictionary<string, string>;
}
catch (Exception)
{
// Ignored
}
}
}
#endif
Dictionary<string, string> newGlobalProperties = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (globalProperties != null)
{
foreach (KeyValuePair<string, string> item in globalProperties)
{
newGlobalProperties[item.Key] = item.Value;
}
}
newGlobalProperties["ExcludeRestorePackageImports"] = bool.TrueString;
newGlobalProperties["OriginalMSBuildStartupDirectory"] = MSBuildStartupDirectory;
if (IsSolutionPathDefined)
{
newGlobalProperties["SolutionPath"] = SolutionPath;
}
return newGlobalProperties;
}
/// <summary>
/// Gets the file name of the process.
/// </summary>
/// <param name="processFileName">An optional process filename to use as an override.</param>
/// <returns>The full path to the file for the process.</returns>
internal string GetProcessFileName(string processFileName)
{
if (!string.IsNullOrEmpty(processFileName))
{
return Path.GetFullPath(processFileName);
}
#if IS_CORECLR
// In .NET Core, the path to dotnet is the file to run
return Path.GetFullPath(Path.Combine(MSBuildBinPath, "..", "..", "dotnet"));
#else
return Path.Combine(ThisAssemblyLazy.Value.DirectoryName, Path.ChangeExtension(ThisAssemblyLazy.Value.Name, ".Console.exe"));
#endif
}
protected virtual void Dispose(bool disposing)
{
_cancellationTokenSource.Dispose();
}
protected virtual Dictionary<string, string> GetOptions()
{
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
[nameof(Recursive)] = Recursive.ToString()
};
}
/// <summary>
/// Writes global properties to the specified stream.
/// </summary>
/// <param name="writer">The <see cref="BinaryWriter" /> to write the global properties to.</param>
/// <param name="globalProperties">A <see cref="Dictionary{TKey, TValue}" /> containing the global properties.</param>
internal static void WriteGlobalProperties(BinaryWriter writer, Dictionary<string, string> globalProperties)
{
writer.Write(globalProperties.Count);
foreach (KeyValuePair<string, string> option in globalProperties)
{
writer.Write(option.Key);
writer.Write(option.Value);
}
}
}
}
|