// 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.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading;
using Microsoft.Extensions.Internal;
using Newtonsoft.Json;
using NuGet.Versioning;
namespace Cli.FunctionalTests.Util
{
internal static class DotNetUtil
{
private const string _clearPackageSourcesNuGetConfig =
@"
";
// Bind to dynamic port 0 to avoid port conflicts during parallel tests
private const string _urls = "--urls http://127.0.0.1:0;https://127.0.0.1:0";
// Must publish to folder under "bin" or "obj" to prevent double-copying publish output during incremental publish
public static string PublishOutput => Path.Combine("bin", "pub");
private static readonly Lazy<(SemanticVersion SdkVersion, SemanticVersion RuntimeVersion)> _versions =
new Lazy<(SemanticVersion SdkVersion, SemanticVersion RuntimeVersion)>(GetVersions, LazyThreadSafetyMode.PublicationOnly);
public static SemanticVersion SdkVersion => _versions.Value.SdkVersion;
public static SemanticVersion RuntimeVersion => _versions.Value.RuntimeVersion;
private static readonly Lazy _requiresPrivateFeed = new Lazy(GetRequiresPrivateFeed, LazyThreadSafetyMode.PublicationOnly);
public static bool RequiresPrivateFeed => _requiresPrivateFeed.Value;
public static string TargetFrameworkMoniker => $"netcoreapp{RuntimeVersion.Major}.{RuntimeVersion.Minor}";
private static readonly HttpClient _httpClient = new HttpClient();
private static readonly IEnumerable> _globalEnvironment = new KeyValuePair[] {
// Ignore globally-installed .NET Core components
new KeyValuePair("DOTNET_MULTILEVEL_LOOKUP", "false"),
};
private static (SemanticVersion SdkVersion, SemanticVersion RuntimeVersion) GetVersions()
{
var info = RunDotNet("--info", workingDirectory: null);
var sdkVersionString = Regex.Match(info, @"Version:\s*(\S+)").Groups[1].Value;
var sdkVersion = SemanticVersion.Parse(sdkVersionString);
// Select highest version of Microsoft.NETCore.App which matches major and minor version of SDK
var runtimeVersionPattern = $@"Microsoft.NETCore.App\s*({sdkVersion.Major}.{sdkVersion.Minor}\S+)";
var runtimeVersionString = Regex.Match(info, runtimeVersionPattern, RegexOptions.RightToLeft).Groups[1].Value;
var runtimeVersion = SemanticVersion.Parse(runtimeVersionString);
// Supported version range is [2.1.300,2.2.100] (inclusive)
if (sdkVersion >= new SemanticVersion(2, 1, 300) && sdkVersion <= new SemanticVersion(2, 2, 100))
{
return (sdkVersion, runtimeVersion);
}
else
{
throw new InvalidOperationException($"Unsupported SDK version: {sdkVersion}");
}
}
// Private feed is required if nuget.org doesn't contain the matching version of Microsoft.NETCore.App
private static bool GetRequiresPrivateFeed()
{
var versionString = _httpClient.GetStringAsync("https://api.nuget.org/v3-flatcontainer/microsoft.netcore.app/index.json").Result;
var definition = new { Versions = Enumerable.Empty() };
var versions = JsonConvert.DeserializeAnonymousType(versionString, definition);
return !versions.Versions.Contains(RuntimeVersion.ToString());
}
private static IEnumerable> GetEnvironment(NuGetPackageSource nuGetPackageSource)
{
// Set NUGET_PACKAGES to an initially-empty, distinct folder for each NuGetPackageSource. This ensures packages are loaded
// from either NuGetFallbackFolder or configured sources, and *not* loaded from the default per-user global-packages folder.
//
// [5/7/2018] NUGET_PACKAGES cannot be set to a folder under the application due to https://github.com/dotnet/cli/issues/9216.
yield return new KeyValuePair("NUGET_PACKAGES", Path.Combine(AssemblySetUp.TempDir, nuGetPackageSource.Name));
}
public static string New(string template, string workingDirectory)
{
// Clear all packages sources by default. May be overridden by NuGetPackageSource parameter.
File.WriteAllText(Path.Combine(workingDirectory, "NuGet.config"), _clearPackageSourcesNuGetConfig);
// Pass "--debug:ephemeral-hive" to build template contents in-memory, rather than using the default
// "%UserProfile%\.templateengine" cache, which may be out-of-date when testing newer builds with the same version.
return RunDotNet($"new {template} --name {template} --output . --no-restore --debug:ephemeral-hive", workingDirectory);
}
public static string Restore(string workingDirectory, NuGetPackageSource packageSource, RuntimeIdentifier runtimeIdentifier)
{
return RunDotNet($"restore /warnaserror --no-cache {packageSource.SourceArgument} {runtimeIdentifier.RuntimeArgument}",
workingDirectory, GetEnvironment(packageSource));
}
public static string Build(string workingDirectory, NuGetPackageSource packageSource, RuntimeIdentifier runtimeIdentifier)
{
// "dotnet build" cannot use "--no-restore" if the app is self-contained and the SDK contains a patched runtime
// https://github.com/dotnet/sdk/issues/2312, https://github.com/dotnet/cli/issues/9514
bool restoreRequired = (runtimeIdentifier != RuntimeIdentifier.None) && (DotNetUtil.RuntimeVersion.Patch > 0);
var restoreArgument = restoreRequired ? $"--no-cache {packageSource.SourceArgument}" : "--no-restore";
return RunDotNet($"build /warnaserror {restoreArgument} {runtimeIdentifier.RuntimeArgument}", workingDirectory, GetEnvironment(packageSource));
}
public static (Process Process, ConcurrentStringBuilder OutputBuilder, ConcurrentStringBuilder ErrorBuilder) Run(
string workingDirectory, RuntimeIdentifier runtimeIdentifier)
{
return StartDotNet($"run --no-build {_urls} {runtimeIdentifier.RuntimeArgument}", workingDirectory);
}
public static string Publish(string workingDirectory, RuntimeIdentifier runtimeIdentifier)
{
return RunDotNet($"publish --no-build -o {PublishOutput} {runtimeIdentifier.RuntimeArgument}", workingDirectory);
}
internal static (Process Process, ConcurrentStringBuilder OutputBuilder, ConcurrentStringBuilder ErrorBuilder) Exec(
string workingDirectory, string name, RuntimeIdentifier runtimeIdentifier)
{
if (runtimeIdentifier == RuntimeIdentifier.None)
{
var path = Path.Combine(PublishOutput, $"{name}.dll");
return StartDotNet($"exec {path} {_urls}", workingDirectory);
}
else
{
var file = (runtimeIdentifier == RuntimeIdentifier.Win_x64) ? $"{name}.exe" : name;
var path = Path.Combine(workingDirectory, PublishOutput, file);
return StartProcess(path, _urls, workingDirectory);
}
}
private static string RunDotNet(string arguments, string workingDirectory,
IEnumerable> environment = null, bool throwOnError = true)
{
var p = StartDotNet(arguments, workingDirectory, environment);
return WaitForExit(p, throwOnError: throwOnError);
}
private static (Process Process, ConcurrentStringBuilder OutputBuilder, ConcurrentStringBuilder ErrorBuilder) StartDotNet(
string arguments, string workingDirectory, IEnumerable> environment = null)
{
var env = _globalEnvironment.Concat(environment ?? Enumerable.Empty>());
return StartProcess("dotnet", arguments, workingDirectory, env);
}
private static (Process Process, ConcurrentStringBuilder OutputBuilder, ConcurrentStringBuilder ErrorBuilder) StartProcess(
string filename, string arguments, string workingDirectory, IEnumerable> environment = null)
{
var process = new Process()
{
StartInfo =
{
FileName = filename,
Arguments = arguments,
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
WorkingDirectory = workingDirectory,
},
};
if (environment != null)
{
foreach (var kvp in environment)
{
process.StartInfo.Environment.Add(kvp);
}
}
var outputBuilder = new ConcurrentStringBuilder();
process.OutputDataReceived += (_, e) =>
{
outputBuilder.AppendLine(e.Data);
};
var errorBuilder = new ConcurrentStringBuilder();
process.ErrorDataReceived += (_, e) =>
{
errorBuilder.AppendLine(e.Data);
};
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
return (process, outputBuilder, errorBuilder);
}
public static string StopProcess((Process Process, ConcurrentStringBuilder OutputBuilder, ConcurrentStringBuilder ErrorBuilder) process,
bool throwOnError = true)
{
if (!process.Process.HasExited)
{
process.Process.KillTree();
}
return WaitForExit(process, throwOnError: throwOnError);
}
public static string WaitForExit((Process Process, ConcurrentStringBuilder OutputBuilder, ConcurrentStringBuilder ErrorBuilder) process,
bool throwOnError = true)
{
// Workaround issue where WaitForExit() blocks until child processes are killed, which is problematic
// for the dotnet.exe NodeReuse child processes. I'm not sure why this is problematic for dotnet.exe child processes
// but not for MSBuild.exe child processes. The workaround is to specify a large timeout.
// https://stackoverflow.com/a/37983587/102052
process.Process.WaitForExit(int.MaxValue);
if (throwOnError && process.Process.ExitCode != 0)
{
var sb = new ConcurrentStringBuilder();
sb.AppendLine($"Command {process.Process.StartInfo.FileName} {process.Process.StartInfo.Arguments} returned exit code {process.Process.ExitCode}");
sb.AppendLine();
sb.AppendLine(process.OutputBuilder.ToString());
throw new InvalidOperationException(sb.ToString());
}
return process.OutputBuilder.ToString();
}
}
}