diff --git a/.gitignore b/.gitignore
index 946d87400a..ada37ce594 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,3 +30,5 @@ project.lock.json
.testPublish/
global.json
korebuild-lock.txt
+msbuild.binlog
+.test-dotnet
diff --git a/AzureIntegration.sln b/AzureIntegration.sln
index 0e01abad26..cdfba4fe53 100644
--- a/AzureIntegration.sln
+++ b/AzureIntegration.sln
@@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
-VisualStudioVersion = 15.0.26412.1
+VisualStudioVersion = 15.0.26814.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.AzureAppServicesIntegration", "src\Microsoft.AspNetCore.AzureAppServicesIntegration\Microsoft.AspNetCore.AzureAppServicesIntegration.csproj", "{5916BEB5-0969-469B-976C-A392E015DFAC}"
EndProject
@@ -35,6 +35,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.AzureA
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApplicationInsights.HostingStartup.Tests", "test\ApplicationInsights.HostingStartup.Tests\ApplicationInsights.HostingStartup.Tests.csproj", "{0899A101-E451-40A4-81B0-7AA18202C25D}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.AzureAppServices.FunctionalTests", "test\Microsoft.AspNetCore.AzureAppServices.FunctionalTests\Microsoft.AspNetCore.AzureAppServices.FunctionalTests.csproj", "{2B2C37FF-9249-4EA4-9A7F-038B55A15C2C}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.AzureAppServices.TestBundle", "src\Microsoft.AspNetCore.AzureAppServices.TestBundle\Microsoft.AspNetCore.AzureAppServices.TestBundle.csproj", "{1EC31DA1-131D-4257-B001-BE8391E6077E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -81,6 +85,14 @@ Global
{0899A101-E451-40A4-81B0-7AA18202C25D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0899A101-E451-40A4-81B0-7AA18202C25D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0899A101-E451-40A4-81B0-7AA18202C25D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2B2C37FF-9249-4EA4-9A7F-038B55A15C2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2B2C37FF-9249-4EA4-9A7F-038B55A15C2C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2B2C37FF-9249-4EA4-9A7F-038B55A15C2C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2B2C37FF-9249-4EA4-9A7F-038B55A15C2C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1EC31DA1-131D-4257-B001-BE8391E6077E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1EC31DA1-131D-4257-B001-BE8391E6077E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1EC31DA1-131D-4257-B001-BE8391E6077E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1EC31DA1-131D-4257-B001-BE8391E6077E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -96,5 +108,10 @@ Global
{9B22E525-FEC9-4C7C-9F9C-598C15BD0250} = {FF9B744E-6C59-40CC-9E41-9D2EBD292435}
{1CE2D76B-39E6-46C0-8F6F-C63E370955A9} = {FF9B744E-6C59-40CC-9E41-9D2EBD292435}
{0899A101-E451-40A4-81B0-7AA18202C25D} = {CD650B4B-81C2-4A44-AEF2-A251A877C1F0}
+ {2B2C37FF-9249-4EA4-9A7F-038B55A15C2C} = {CD650B4B-81C2-4A44-AEF2-A251A877C1F0}
+ {1EC31DA1-131D-4257-B001-BE8391E6077E} = {FF9B744E-6C59-40CC-9E41-9D2EBD292435}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {5743DFE7-1AA5-439D-84AE-A480EA389927}
EndGlobalSection
EndGlobal
diff --git a/build/dependencies.props b/build/dependencies.props
index aff9dda687..9484451623 100644
--- a/build/dependencies.props
+++ b/build/dependencies.props
@@ -5,11 +5,13 @@
2.1.1
2.1.1-*
4.7.49
+ 1.1.3
2.0.0-*
2.0.0-*
2.0.0-*
15.3.0
1.4.0
2.3.0-beta4-build3742
+ 8.3.0
diff --git a/build/dotnet-install.cmd b/build/dotnet-install.cmd
new file mode 100644
index 0000000000..1dbf49f0a1
--- /dev/null
+++ b/build/dotnet-install.cmd
@@ -0,0 +1,2 @@
+@ECHO OFF
+PowerShell -NoProfile -NoLogo -ExecutionPolicy unrestricted -Command "& '%~dp0dotnet-install.ps1' %*; exit $LASTEXITCODE"
diff --git a/build/dotnet-install.ps1 b/build/dotnet-install.ps1
new file mode 100644
index 0000000000..93d964540f
--- /dev/null
+++ b/build/dotnet-install.ps1
@@ -0,0 +1,503 @@
+#
+# 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.
+#
+
+<#
+.SYNOPSIS
+ Installs dotnet cli
+.DESCRIPTION
+ Installs dotnet cli. If dotnet installation already exists in the given directory
+ it will update it only if the requested version differs from the one already installed.
+.PARAMETER Channel
+ Default: LTS
+ Download from the Channel specified. Possible values:
+ - Current - most current release
+ - LTS - most current supported release
+ - 2-part version in a format A.B - represents a specific release
+ examples: 2.0; 1.0
+ - Branch name
+ examples: release/2.0.0; Master
+.PARAMETER Version
+ Default: latest
+ Represents a build version on specific channel. Possible values:
+ - latest - most latest build on specific channel
+ - coherent - most latest coherent build on specific channel
+ coherent applies only to SDK downloads
+ - 3-part version in a format A.B.C - represents specific version of build
+ examples: 2.0.0-preview2-006120; 1.1.0
+.PARAMETER InstallDir
+ Default: %LocalAppData%\Microsoft\dotnet
+ Path to where to install dotnet. Note that binaries will be placed directly in a given directory.
+.PARAMETER Architecture
+ Default: - this value represents currently running OS architecture
+ Architecture of dotnet binaries to be installed.
+ Possible values are: , x64 and x86
+.PARAMETER SharedRuntime
+ Default: false
+ Installs just the shared runtime bits, not the entire SDK
+.PARAMETER DryRun
+ If set it will not perform installation but instead display what command line to use to consistently install
+ currently requested version of dotnet cli. In example if you specify version 'latest' it will display a link
+ with specific version so that this command can be used deterministicly in a build script.
+ It also displays binaries location if you prefer to install or download it yourself.
+.PARAMETER NoPath
+ By default this script will set environment variable PATH for the current process to the binaries folder inside installation folder.
+ If set it will display binaries location but not set any environment variable.
+.PARAMETER Verbose
+ Displays diagnostics information.
+.PARAMETER AzureFeed
+ Default: https://dotnetcli.azureedge.net/dotnet
+ This parameter typically is not changed by the user.
+ It allows to change URL for the Azure feed used by this installer.
+.PARAMETER UncachedFeed
+ This parameter typically is not changed by the user.
+ It allows to change URL for the Uncached feed used by this installer.
+.PARAMETER ProxyAddress
+ If set, the installer will use the proxy when making web requests
+.PARAMETER ProxyUseDefaultCredentials
+ Default: false
+ Use default credentials, when using proxy address.
+#>
+[cmdletbinding()]
+param(
+ [string]$Channel="LTS",
+ [string]$Version="Latest",
+ [string]$InstallDir="",
+ [string]$Architecture="",
+ [switch]$SharedRuntime,
+ [switch]$DryRun,
+ [switch]$NoPath,
+ [string]$AzureFeed="https://dotnetcli.azureedge.net/dotnet",
+ [string]$UncachedFeed="https://dotnetcli.blob.core.windows.net/dotnet",
+ [string]$ProxyAddress,
+ [switch]$ProxyUseDefaultCredentials
+)
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference="Stop"
+$ProgressPreference="SilentlyContinue"
+
+$BinFolderRelativePath=""
+
+# example path with regex: shared/1.0.0-beta-12345/somepath
+$VersionRegEx="/\d+\.\d+[^/]+/"
+$OverrideNonVersionedFiles=$true
+
+function Say($str) {
+ Write-Output "dotnet-install: $str"
+}
+
+function Say-Verbose($str) {
+ Write-Verbose "dotnet-install: $str"
+}
+
+function Say-Invocation($Invocation) {
+ $command = $Invocation.MyCommand;
+ $args = (($Invocation.BoundParameters.Keys | foreach { "-$_ `"$($Invocation.BoundParameters[$_])`"" }) -join " ")
+ Say-Verbose "$command $args"
+}
+
+function Invoke-With-Retry([ScriptBlock]$ScriptBlock, [int]$MaxAttempts = 3, [int]$SecondsBetweenAttempts = 1) {
+ $Attempts = 0
+
+ while ($true) {
+ try {
+ return $ScriptBlock.Invoke()
+ }
+ catch {
+ $Attempts++
+ if ($Attempts -lt $MaxAttempts) {
+ Start-Sleep $SecondsBetweenAttempts
+ }
+ else {
+ throw
+ }
+ }
+ }
+}
+
+function Get-Machine-Architecture() {
+ Say-Invocation $MyInvocation
+
+ # possible values: AMD64, IA64, x86
+ return $ENV:PROCESSOR_ARCHITECTURE
+}
+
+# TODO: Architecture and CLIArchitecture should be unified
+function Get-CLIArchitecture-From-Architecture([string]$Architecture) {
+ Say-Invocation $MyInvocation
+
+ switch ($Architecture.ToLower()) {
+ { $_ -eq "" } { return Get-CLIArchitecture-From-Architecture $(Get-Machine-Architecture) }
+ { ($_ -eq "amd64") -or ($_ -eq "x64") } { return "x64" }
+ { $_ -eq "x86" } { return "x86" }
+ default { throw "Architecture not supported. If you think this is a bug, please report it at https://github.com/dotnet/cli/issues" }
+ }
+}
+
+function Get-Version-Info-From-Version-Text([string]$VersionText) {
+ Say-Invocation $MyInvocation
+
+ $Data = @($VersionText.Split([char[]]@(), [StringSplitOptions]::RemoveEmptyEntries));
+
+ $VersionInfo = @{}
+ $VersionInfo.CommitHash = $Data[0].Trim()
+ $VersionInfo.Version = $Data[1].Trim()
+ return $VersionInfo
+}
+
+function Load-Assembly([string] $Assembly) {
+ try {
+ Add-Type -Assembly $Assembly | Out-Null
+ }
+ catch {
+ # On Nano Server, Powershell Core Edition is used. Add-Type is unable to resolve base class assemblies because they are not GAC'd.
+ # Loading the base class assemblies is not unnecessary as the types will automatically get resolved.
+ }
+}
+
+function GetHTTPResponse([Uri] $Uri)
+{
+ Invoke-With-Retry(
+ {
+
+ $HttpClient = $null
+
+ try {
+ # HttpClient is used vs Invoke-WebRequest in order to support Nano Server which doesn't support the Invoke-WebRequest cmdlet.
+ Load-Assembly -Assembly System.Net.Http
+
+ if(-not $ProxyAddress)
+ {
+ # Despite no proxy being explicitly specified, we may still be behind a default proxy
+ $DefaultProxy = [System.Net.WebRequest]::DefaultWebProxy;
+ if($DefaultProxy -and (-not $DefaultProxy.IsBypassed($Uri))){
+ $ProxyAddress = $DefaultProxy.GetProxy($Uri).OriginalString
+ $ProxyUseDefaultCredentials = $true
+ }
+ }
+
+ if($ProxyAddress){
+ $HttpClientHandler = New-Object System.Net.Http.HttpClientHandler
+ $HttpClientHandler.Proxy = New-Object System.Net.WebProxy -Property @{Address=$ProxyAddress;UseDefaultCredentials=$ProxyUseDefaultCredentials}
+ $HttpClient = New-Object System.Net.Http.HttpClient -ArgumentList $HttpClientHandler
+ }
+ else {
+ $HttpClient = New-Object System.Net.Http.HttpClient
+ }
+ # Default timeout for HttpClient is 100s. For a 50 MB download this assumes 500 KB/s average, any less will time out
+ # 10 minutes allows it to work over much slower connections.
+ $HttpClient.Timeout = New-TimeSpan -Minutes 10
+ $Response = $HttpClient.GetAsync($Uri).Result
+ if (($Response -eq $null) -or (-not ($Response.IsSuccessStatusCode)))
+ {
+ $ErrorMsg = "Failed to download $Uri."
+ if ($Response -ne $null)
+ {
+ $ErrorMsg += " $Response"
+ }
+
+ throw $ErrorMsg
+ }
+
+ return $Response
+ }
+ finally {
+ if ($HttpClient -ne $null) {
+ $HttpClient.Dispose()
+ }
+ }
+ })
+}
+
+
+function Get-Latest-Version-Info([string]$AzureFeed, [string]$Channel, [bool]$Coherent) {
+ Say-Invocation $MyInvocation
+
+ $VersionFileUrl = $null
+ if ($SharedRuntime) {
+ $VersionFileUrl = "$UncachedFeed/Runtime/$Channel/latest.version"
+ }
+ else {
+ if ($Coherent) {
+ $VersionFileUrl = "$UncachedFeed/Sdk/$Channel/latest.coherent.version"
+ }
+ else {
+ $VersionFileUrl = "$UncachedFeed/Sdk/$Channel/latest.version"
+ }
+ }
+
+ $Response = GetHTTPResponse -Uri $VersionFileUrl
+ $StringContent = $Response.Content.ReadAsStringAsync().Result
+
+ switch ($Response.Content.Headers.ContentType) {
+ { ($_ -eq "application/octet-stream") } { $VersionText = [Text.Encoding]::UTF8.GetString($StringContent) }
+ { ($_ -eq "text/plain") } { $VersionText = $StringContent }
+ { ($_ -eq "text/plain; charset=UTF-8") } { $VersionText = $StringContent }
+ default { throw "``$Response.Content.Headers.ContentType`` is an unknown .version file content type." }
+ }
+
+ $VersionInfo = Get-Version-Info-From-Version-Text $VersionText
+
+ return $VersionInfo
+}
+
+
+function Get-Specific-Version-From-Version([string]$AzureFeed, [string]$Channel, [string]$Version) {
+ Say-Invocation $MyInvocation
+
+ switch ($Version.ToLower()) {
+ { $_ -eq "latest" } {
+ $LatestVersionInfo = Get-Latest-Version-Info -AzureFeed $AzureFeed -Channel $Channel -Coherent $False
+ return $LatestVersionInfo.Version
+ }
+ { $_ -eq "coherent" } {
+ $LatestVersionInfo = Get-Latest-Version-Info -AzureFeed $AzureFeed -Channel $Channel -Coherent $True
+ return $LatestVersionInfo.Version
+ }
+ default { return $Version }
+ }
+}
+
+function Get-Download-Link([string]$AzureFeed, [string]$Channel, [string]$SpecificVersion, [string]$CLIArchitecture) {
+ Say-Invocation $MyInvocation
+
+ if ($SharedRuntime) {
+ $PayloadURL = "$AzureFeed/Runtime/$SpecificVersion/dotnet-runtime-$SpecificVersion-win-$CLIArchitecture.zip"
+ }
+ else {
+ $PayloadURL = "$AzureFeed/Sdk/$SpecificVersion/dotnet-sdk-$SpecificVersion-win-$CLIArchitecture.zip"
+ }
+
+ Say-Verbose "Constructed primary payload URL: $PayloadURL"
+
+ return $PayloadURL
+}
+
+function Get-LegacyDownload-Link([string]$AzureFeed, [string]$Channel, [string]$SpecificVersion, [string]$CLIArchitecture) {
+ Say-Invocation $MyInvocation
+
+ if ($SharedRuntime) {
+ $PayloadURL = "$AzureFeed/Runtime/$SpecificVersion/dotnet-win-$CLIArchitecture.$SpecificVersion.zip"
+ }
+ else {
+ $PayloadURL = "$AzureFeed/Sdk/$SpecificVersion/dotnet-dev-win-$CLIArchitecture.$SpecificVersion.zip"
+ }
+
+ Say-Verbose "Constructed legacy payload URL: $PayloadURL"
+
+ return $PayloadURL
+}
+
+function Get-User-Share-Path() {
+ Say-Invocation $MyInvocation
+
+ $InstallRoot = $env:DOTNET_INSTALL_DIR
+ if (!$InstallRoot) {
+ $InstallRoot = "$env:LocalAppData\Microsoft\dotnet"
+ }
+ return $InstallRoot
+}
+
+function Resolve-Installation-Path([string]$InstallDir) {
+ Say-Invocation $MyInvocation
+
+ if ($InstallDir -eq "") {
+ return Get-User-Share-Path
+ }
+ return $InstallDir
+}
+
+function Get-Version-Info-From-Version-File([string]$InstallRoot, [string]$RelativePathToVersionFile) {
+ Say-Invocation $MyInvocation
+
+ $VersionFile = Join-Path -Path $InstallRoot -ChildPath $RelativePathToVersionFile
+ Say-Verbose "Local version file: $VersionFile"
+
+ if (Test-Path $VersionFile) {
+ $VersionText = cat $VersionFile
+ Say-Verbose "Local version file text: $VersionText"
+ return Get-Version-Info-From-Version-Text $VersionText
+ }
+
+ Say-Verbose "Local version file not found."
+
+ return $null
+}
+
+function Is-Dotnet-Package-Installed([string]$InstallRoot, [string]$RelativePathToPackage, [string]$SpecificVersion) {
+ Say-Invocation $MyInvocation
+
+ $DotnetPackagePath = Join-Path -Path $InstallRoot -ChildPath $RelativePathToPackage | Join-Path -ChildPath $SpecificVersion
+ Say-Verbose "Is-Dotnet-Package-Installed: Path to a package: $DotnetPackagePath"
+ return Test-Path $DotnetPackagePath -PathType Container
+}
+
+function Get-Absolute-Path([string]$RelativeOrAbsolutePath) {
+ # Too much spam
+ # Say-Invocation $MyInvocation
+
+ return $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($RelativeOrAbsolutePath)
+}
+
+function Get-Path-Prefix-With-Version($path) {
+ $match = [regex]::match($path, $VersionRegEx)
+ if ($match.Success) {
+ return $entry.FullName.Substring(0, $match.Index + $match.Length)
+ }
+
+ return $null
+}
+
+function Get-List-Of-Directories-And-Versions-To-Unpack-From-Dotnet-Package([System.IO.Compression.ZipArchive]$Zip, [string]$OutPath) {
+ Say-Invocation $MyInvocation
+
+ $ret = @()
+ foreach ($entry in $Zip.Entries) {
+ $dir = Get-Path-Prefix-With-Version $entry.FullName
+ if ($dir -ne $null) {
+ $path = Get-Absolute-Path $(Join-Path -Path $OutPath -ChildPath $dir)
+ if (-Not (Test-Path $path -PathType Container)) {
+ $ret += $dir
+ }
+ }
+ }
+
+ $ret = $ret | Sort-Object | Get-Unique
+
+ $values = ($ret | foreach { "$_" }) -join ";"
+ Say-Verbose "Directories to unpack: $values"
+
+ return $ret
+}
+
+# Example zip content and extraction algorithm:
+# Rule: files if extracted are always being extracted to the same relative path locally
+# .\
+# a.exe # file does not exist locally, extract
+# b.dll # file exists locally, override only if $OverrideFiles set
+# aaa\ # same rules as for files
+# ...
+# abc\1.0.0\ # directory contains version and exists locally
+# ... # do not extract content under versioned part
+# abc\asd\ # same rules as for files
+# ...
+# def\ghi\1.0.1\ # directory contains version and does not exist locally
+# ... # extract content
+function Extract-Dotnet-Package([string]$ZipPath, [string]$OutPath) {
+ Say-Invocation $MyInvocation
+
+ Load-Assembly -Assembly System.IO.Compression.FileSystem
+ Set-Variable -Name Zip
+ try {
+ $Zip = [System.IO.Compression.ZipFile]::OpenRead($ZipPath)
+
+ $DirectoriesToUnpack = Get-List-Of-Directories-And-Versions-To-Unpack-From-Dotnet-Package -Zip $Zip -OutPath $OutPath
+
+ foreach ($entry in $Zip.Entries) {
+ $PathWithVersion = Get-Path-Prefix-With-Version $entry.FullName
+ if (($PathWithVersion -eq $null) -Or ($DirectoriesToUnpack -contains $PathWithVersion)) {
+ $DestinationPath = Get-Absolute-Path $(Join-Path -Path $OutPath -ChildPath $entry.FullName)
+ $DestinationDir = Split-Path -Parent $DestinationPath
+ $OverrideFiles=$OverrideNonVersionedFiles -Or (-Not (Test-Path $DestinationPath))
+ if ((-Not $DestinationPath.EndsWith("\")) -And $OverrideFiles) {
+ New-Item -ItemType Directory -Force -Path $DestinationDir | Out-Null
+ [System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $DestinationPath, $OverrideNonVersionedFiles)
+ }
+ }
+ }
+ }
+ finally {
+ if ($Zip -ne $null) {
+ $Zip.Dispose()
+ }
+ }
+}
+
+function DownloadFile([Uri]$Uri, [string]$OutPath) {
+ $Stream = $null
+
+ try {
+ $Response = GetHTTPResponse -Uri $Uri
+ $Stream = $Response.Content.ReadAsStreamAsync().Result
+ $File = [System.IO.File]::Create($OutPath)
+ $Stream.CopyTo($File)
+ $File.Close()
+ }
+ finally {
+ if ($Stream -ne $null) {
+ $Stream.Dispose()
+ }
+ }
+}
+
+function Prepend-Sdk-InstallRoot-To-Path([string]$InstallRoot, [string]$BinFolderRelativePath) {
+ $BinPath = Get-Absolute-Path $(Join-Path -Path $InstallRoot -ChildPath $BinFolderRelativePath)
+ if (-Not $NoPath) {
+ Say "Adding to current process PATH: `"$BinPath`". Note: This change will not be visible if PowerShell was run as a child process."
+ $env:path = "$BinPath;" + $env:path
+ }
+ else {
+ Say "Binaries of dotnet can be found in $BinPath"
+ }
+}
+
+$CLIArchitecture = Get-CLIArchitecture-From-Architecture $Architecture
+$SpecificVersion = Get-Specific-Version-From-Version -AzureFeed $AzureFeed -Channel $Channel -Version $Version
+$DownloadLink = Get-Download-Link -AzureFeed $AzureFeed -Channel $Channel -SpecificVersion $SpecificVersion -CLIArchitecture $CLIArchitecture
+$LegacyDownloadLink = Get-LegacyDownload-Link -AzureFeed $AzureFeed -Channel $Channel -SpecificVersion $SpecificVersion -CLIArchitecture $CLIArchitecture
+
+if ($DryRun) {
+ Say "Payload URLs:"
+ Say "Primary - $DownloadLink"
+ Say "Legacy - $LegacyDownloadLink"
+ Say "Repeatable invocation: .\$($MyInvocation.MyCommand) -Version $SpecificVersion -Channel $Channel -Architecture $CLIArchitecture -InstallDir $InstallDir"
+ exit 0
+}
+
+$InstallRoot = Resolve-Installation-Path $InstallDir
+Say-Verbose "InstallRoot: $InstallRoot"
+
+$IsSdkInstalled = Is-Dotnet-Package-Installed -InstallRoot $InstallRoot -RelativePathToPackage "sdk" -SpecificVersion $SpecificVersion
+Say-Verbose ".NET SDK installed? $IsSdkInstalled"
+if ($IsSdkInstalled) {
+ Say ".NET SDK version $SpecificVersion is already installed."
+ Prepend-Sdk-InstallRoot-To-Path -InstallRoot $InstallRoot -BinFolderRelativePath $BinFolderRelativePath
+ exit 0
+}
+
+New-Item -ItemType Directory -Force -Path $InstallRoot | Out-Null
+
+$installDrive = $((Get-Item $InstallRoot).PSDrive.Name);
+Write-Output "${installDrive}:";
+$free = Get-CimInstance -Class win32_logicaldisk | where Deviceid -eq "${installDrive}:"
+if ($free.Freespace / 1MB -le 100 ) {
+ Say "There is not enough disk space on drive ${installDrive}:"
+ exit 0
+}
+
+$ZipPath = [System.IO.Path]::GetTempFileName()
+Say-Verbose "Zip path: $ZipPath"
+Say "Downloading link: $DownloadLink"
+try {
+ DownloadFile -Uri $DownloadLink -OutPath $ZipPath
+}
+catch {
+ Say "Cannot download: $DownloadLink"
+ $DownloadLink = $LegacyDownloadLink
+ $ZipPath = [System.IO.Path]::GetTempFileName()
+ Say-Verbose "Legacy zip path: $ZipPath"
+ Say "Downloading legacy link: $DownloadLink"
+ DownloadFile -Uri $DownloadLink -OutPath $ZipPath
+}
+
+Say "Extracting zip from $DownloadLink"
+Extract-Dotnet-Package -ZipPath $ZipPath -OutPath $InstallRoot
+
+Remove-Item $ZipPath
+
+Prepend-Sdk-InstallRoot-To-Path -InstallRoot $InstallRoot -BinFolderRelativePath $BinFolderRelativePath
+
+Say "Installation finished"
+exit 0
diff --git a/build/repo.props b/build/repo.props
new file mode 100644
index 0000000000..3b59246633
--- /dev/null
+++ b/build/repo.props
@@ -0,0 +1,17 @@
+
+
+ $(RepositoryRoot)test\Microsoft.AspNetCore.AzureAppServices.FunctionalTests\Microsoft.AspNetCore.AzureAppServices.FunctionalTests.csproj
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build/repo.targets b/build/repo.targets
new file mode 100644
index 0000000000..9460d8b735
--- /dev/null
+++ b/build/repo.targets
@@ -0,0 +1,39 @@
+
+
+
+ $(RepositoryRoot)src\Microsoft.AspNetCore.AzureAppServices.TestBundle\
+ https://dotnet.myget.org/F/aspnetcore-ci-dev/
+ master
+ coherent
+
+
+
+
+
+ $(RepositoryRoot).test-dotnet\
+ build\dotnet.version
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.AspNetCore.AzureAppServices.TestBundle/Microsoft.AspNetCore.AzureAppServices.TestBundle.csproj b/src/Microsoft.AspNetCore.AzureAppServices.TestBundle/Microsoft.AspNetCore.AzureAppServices.TestBundle.csproj
new file mode 100644
index 0000000000..2d8fc6b90a
--- /dev/null
+++ b/src/Microsoft.AspNetCore.AzureAppServices.TestBundle/Microsoft.AspNetCore.AzureAppServices.TestBundle.csproj
@@ -0,0 +1,35 @@
+
+
+
+
+
+ ASP.NET Core Test Bundle Extensions
+ This extension enables testing functionality of ASP.NET Core on Azure WebSites.
+ net461
+ false
+ aspnet;logging;aspnetcore;AzureSiteExtension
+ AzureSiteExtension
+ true
+ false
+ false
+ false
+ content
+ AspNetCoreTestBundle
+
+ https://github.com/aspnet/AzureIntegration/blob/rel/2.0.0-preview1/LICENSE.txt
+ https://go.microsoft.com/fwlink/?LinkID=288859
+ https://www.asp.net/
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.AspNetCore.AzureAppServices.TestBundle/applicationHost.xdt b/src/Microsoft.AspNetCore.AzureAppServices.TestBundle/applicationHost.xdt
new file mode 100644
index 0000000000..5a7f96ddd8
--- /dev/null
+++ b/src/Microsoft.AspNetCore.AzureAppServices.TestBundle/applicationHost.xdt
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/AzureCollection.cs b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/AzureCollection.cs
new file mode 100644
index 0000000000..8e2e110b81
--- /dev/null
+++ b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/AzureCollection.cs
@@ -0,0 +1,15 @@
+// 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 Xunit;
+
+namespace Microsoft.AspNetCore.AzureAppServices.FunctionalTests
+{
+ [CollectionDefinition("Azure")]
+ public class AzureCollection : ICollectionFixture
+ {
+ // This class has no code, and is never created. Its purpose is simply
+ // to be the place to apply [CollectionDefinition] and all the
+ // ICollectionFixture<> interfaces.
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/AzureFixture.cs b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/AzureFixture.cs
new file mode 100644
index 0000000000..5f6dd9e656
--- /dev/null
+++ b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/AzureFixture.cs
@@ -0,0 +1,154 @@
+// 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.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
+using Microsoft.Azure.Management.AppService.Fluent;
+using Microsoft.Azure.Management.Fluent;
+using Microsoft.Azure.Management.ResourceManager.Fluent;
+using Microsoft.Azure.Management.ResourceManager.Fluent.Core;
+using Microsoft.Azure.Management.ResourceManager.Fluent.Models;
+using Microsoft.Azure.Management.Storage.Fluent;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Testing;
+using Microsoft.Rest;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.AspNetCore.AzureAppServices.FunctionalTests
+{
+ public class AzureFixture : IDisposable
+ {
+ public string Timestamp { get; set; }
+
+ public AzureFixture()
+ {
+ TestLog = AssemblyTestLog.ForAssembly(typeof(AzureFixture).Assembly);
+
+ // TODO: Temporary to see if it's useful and worth exposing
+ var globalLoggerFactory =
+ (ILoggerFactory) TestLog.GetType().GetField("_globalLoggerFactory", BindingFlags.NonPublic | BindingFlags.Instance)
+ .GetValue(TestLog);
+
+ var logger = globalLoggerFactory.CreateLogger();
+
+ ServiceClientTracing.IsEnabled = true;
+ ServiceClientTracing.AddTracingInterceptor(new LoggingInterceptor(globalLoggerFactory.CreateLogger(nameof(ServiceClientTracing))));
+
+ var clientId = GetRequiredEnvironmentVariable("AZURE_AUTH_CLIENT_ID");
+ var clientSecret = GetRequiredEnvironmentVariable("AZURE_AUTH_CLIENT_SECRET");
+ var tenant = GetRequiredEnvironmentVariable("AZURE_AUTH_TENANT");
+
+ var credentials = SdkContext.AzureCredentialsFactory.FromServicePrincipal(clientId, clientSecret, tenant, AzureEnvironment.AzureGlobalCloud);
+ Azure = Microsoft.Azure.Management.Fluent.Azure.Configure()
+ .WithLogLevel(HttpLoggingDelegatingHandler.Level.Basic)
+ .Authenticate(credentials)
+ .WithDefaultSubscription();
+
+ Timestamp = DateTime.Now.ToString("yyyyMMddhhmmss");
+ var testRunName = GetTimestampedName("FunctionalTests");
+
+ logger.LogInformation("Creating resource group {TestRunName}", testRunName);
+ ResourceGroup = Azure.ResourceGroups
+ .Define(testRunName)
+ .WithRegion(Region.USWest2)
+ .Create();
+
+ var servicePlanName = GetTimestampedName("TestPlan");
+ logger.LogInformation("Creating service plan {servicePlanName}", testRunName);
+
+ Plan = Azure.AppServices.AppServicePlans.Define(servicePlanName)
+ .WithRegion(Region.USWest2)
+ .WithExistingResourceGroup(ResourceGroup)
+ .WithFreePricingTier()
+ .Create();
+ }
+
+ private static string GetRequiredEnvironmentVariable(string name)
+ {
+ var authFile = Environment.GetEnvironmentVariable(name);
+ if (string.IsNullOrEmpty(authFile))
+ {
+ throw new InvalidOperationException($"{name} environment variable has to be set to run these tests.");
+ }
+
+ return authFile;
+ }
+
+ public IAppServicePlan Plan { get; set; }
+
+ public IStorageAccount DeploymentStorageAccount { get; set; }
+
+ public AssemblyTestLog TestLog { get; set; }
+
+ public bool DeleteResourceGroup { get; set; } = true;
+
+ public IResourceGroup ResourceGroup { get; set; }
+
+ public IAzure Azure { get; set; }
+
+ public string GetTimestampedName(string name)
+ {
+ return name + Timestamp;
+ }
+
+ public async Task Deploy(string template, IDictionary additionalArguments = null, [CallerMemberName] string baseName = null)
+ {
+ var siteName = GetTimestampedName(baseName);
+ var parameters = new Dictionary
+ {
+ {"siteName", siteName},
+ {"hostingPlanName", Plan.Name},
+ {"resourceGroupName", ResourceGroup.Name},
+ };
+
+ foreach (var pair in additionalArguments ?? Enumerable.Empty>())
+ {
+ parameters[pair.Key] = pair.Value;
+ }
+
+ var readAllText = File.ReadAllText(template);
+ var deployment = await Azure.Deployments.Define(GetTimestampedName("Deployment"))
+ .WithExistingResourceGroup(ResourceGroup)
+ .WithTemplate(readAllText)
+ .WithParameters(ToParametersObject(parameters))
+ .WithMode(DeploymentMode.Incremental)
+ .CreateAsync();
+
+ deployment = await deployment.RefreshAsync();
+
+ var outputs = (JObject)deployment.Outputs;
+
+ var siteIdOutput = outputs["siteId"];
+ if (siteIdOutput == null)
+ {
+ throw new InvalidOperationException("Deployment was expected to have 'siteId' output parameter");
+ }
+ var siteId = siteIdOutput["value"].Value();
+ return await Azure.AppServices.WebApps.GetByIdAsync(siteId);
+ }
+
+ private JObject ToParametersObject(Dictionary parameters)
+ {
+ return new JObject(
+ parameters.Select(parameter =>
+ new JProperty(
+ parameter.Key,
+ new JObject(
+ new JProperty("value", parameter.Value)))));
+ }
+
+ public void Dispose()
+ {
+ TestLog.Dispose();
+ if (DeleteResourceGroup && ResourceGroup != null)
+ {
+ Azure.ResourceGroups.DeleteByName(ResourceGroup.Name);
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/CommandResult.cs b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/CommandResult.cs
new file mode 100644
index 0000000000..bae995afe1
--- /dev/null
+++ b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/CommandResult.cs
@@ -0,0 +1,32 @@
+// 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.Diagnostics;
+using Xunit;
+
+namespace Microsoft.AspNetCore.AzureAppServices.FunctionalTests
+{
+ public struct CommandResult
+ {
+ public static readonly CommandResult Empty = new CommandResult();
+
+ public ProcessStartInfo StartInfo { get; }
+ public int ExitCode { get; }
+ public string StdOut { get; }
+ public string StdErr { get; }
+
+ public CommandResult(ProcessStartInfo startInfo, int exitCode, string stdOut, string stdErr)
+ {
+ StartInfo = startInfo;
+ ExitCode = exitCode;
+ StdOut = stdOut;
+ StdErr = stdErr;
+ }
+
+ public void AssertSuccess()
+ {
+ Assert.True(0 == ExitCode, StdOut + Environment.NewLine + StdErr + Environment.NewLine);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/LoggingInterceptor.cs b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/LoggingInterceptor.cs
new file mode 100644
index 0000000000..7d05212d50
--- /dev/null
+++ b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/LoggingInterceptor.cs
@@ -0,0 +1,47 @@
+// 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.Net.Http;
+using Microsoft.Extensions.Logging;
+using Microsoft.Rest;
+
+namespace Microsoft.AspNetCore.AzureAppServices.FunctionalTests
+{
+ public class LoggingInterceptor : IServiceClientTracingInterceptor
+ {
+ private readonly ILogger _logger;
+
+ public LoggingInterceptor(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ public void Information(string message)
+ {
+ _logger.LogInformation(message);
+ }
+
+ public void TraceError(string invocationId, Exception exception)
+ {
+ _logger.LogInformation(exception, "Exception in {invocationId}", invocationId);
+ }
+
+ public void ReceiveResponse(string invocationId, HttpResponseMessage response)
+ {
+ _logger.LogInformation(response.AsFormattedString());
+ }
+
+ public void SendRequest(string invocationId, HttpRequestMessage request)
+ {
+ _logger.LogInformation(request.AsFormattedString());
+ }
+
+ public void Configuration(string source, string name, string value) { }
+
+ public void EnterMethod(string invocationId, object instance, string method, IDictionary parameters) { }
+
+ public void ExitMethod(string invocationId, object returnValue) { }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/Microsoft.AspNetCore.AzureAppServices.FunctionalTests.csproj b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/Microsoft.AspNetCore.AzureAppServices.FunctionalTests.csproj
new file mode 100644
index 0000000000..00f697511e
--- /dev/null
+++ b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/Microsoft.AspNetCore.AzureAppServices.FunctionalTests.csproj
@@ -0,0 +1,27 @@
+
+
+
+
+
+ net461
+ 7.1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/TemplateFunctionalTests.cs b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/TemplateFunctionalTests.cs
new file mode 100644
index 0000000000..e92e6b6637
--- /dev/null
+++ b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/TemplateFunctionalTests.cs
@@ -0,0 +1,81 @@
+// 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.IO;
+using System.Net.Http;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
+using Microsoft.Azure.Management.AppService.Fluent;
+using Microsoft.Azure.Management.AppService.Fluent.Models;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Microsoft.AspNetCore.AzureAppServices.FunctionalTests
+{
+ [Collection("Azure")]
+ public class TemplateFunctionalTests
+ {
+ readonly AzureFixture _fixture;
+
+ private readonly ITestOutputHelper _outputHelper;
+
+ public TemplateFunctionalTests(AzureFixture fixture, ITestOutputHelper outputHelper)
+ {
+ _fixture = fixture;
+ _outputHelper = outputHelper;
+ }
+
+ [Fact]
+ public async Task DotnetNewWebRunsInWebApp()
+ {
+ using (var logger = GetLogger())
+ {
+ Assert.NotNull(_fixture.Azure);
+
+ var site = await _fixture.Deploy("Templates\\BasicAppServices.json", null);
+ var testDirectory = GetTestDirectory();
+
+ var dotnet = DotNet(logger, testDirectory);
+
+ var result = await dotnet.ExecuteAsync("new web");
+ result.AssertSuccess();
+
+ await site.BuildPublishProfileAsync(testDirectory.FullName);
+
+ result = await dotnet.ExecuteAsync("publish /p:PublishProfile=Profile");
+ result.AssertSuccess();
+
+ using (var httpClient = site.CreateClient())
+ {
+ var getResult = await httpClient.GetAsync("/");
+ getResult.EnsureSuccessStatusCode();
+ Assert.Equal("Hello World!", await getResult.Content.ReadAsStringAsync());
+ }
+ }
+ }
+
+ private TestLogger GetLogger([CallerMemberName] string callerName = null)
+ {
+ _fixture.TestLog.StartTestLog(_outputHelper, nameof(TemplateFunctionalTests), out var factory, callerName);
+ return new TestLogger(factory, factory.CreateLogger(callerName));
+ }
+
+ private TestCommand DotNet(TestLogger logger, DirectoryInfo workingDirectory)
+ {
+ return new TestCommand("dotnet")
+ {
+ Logger = logger,
+ WorkingDirectory = workingDirectory.FullName
+ };
+ }
+
+ private DirectoryInfo GetTestDirectory([CallerMemberName] string callerName = null)
+ {
+ if (Directory.Exists(callerName))
+ {
+ Directory.Delete(callerName, recursive:true);
+ }
+ return Directory.CreateDirectory(callerName);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/Templates/AppServicesWithSiteExtensions.json b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/Templates/AppServicesWithSiteExtensions.json
new file mode 100644
index 0000000000..9f0d6c8528
--- /dev/null
+++ b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/Templates/AppServicesWithSiteExtensions.json
@@ -0,0 +1,56 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "parameters": {
+ "siteName": {
+ "type": "string"
+ },
+ "hostingPlanName": {
+ "type": "string"
+ },
+ "resourceGroupName": {
+ "type": "string"
+ },
+ "extensionFeed": {
+ "type": "string"
+ },
+ "extensionName": {
+ "type": "string"
+ },
+ "extensionVersion": {
+ "type": "string"
+ }
+ },
+ "resources": [
+ {
+ "apiVersion": "2015-08-01",
+ "name": "[parameters('siteName')]",
+ "type": "Microsoft.Web/sites",
+ "location": "West US 2",
+ "properties": {
+ "serverFarmId": "[resourceId(parameters('resourceGroupName'), 'Microsoft.Web/serverFarms', parameters('hostingPlanName'))]"
+ },
+ "resources": [
+ {
+ "type": "extensions",
+ "name": "[parameters('extensionName')]",
+ "apiVersion": "2015-08-01",
+ "location": "West US 2",
+ "properties": {
+ "version": "[parameters('extensionVersion')]",
+ "feed_url": "[parameters('extensionFeed')]"
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Web/Sites', parameters('siteName'))]"
+ ]
+ }
+ ]
+ }
+ ],
+ "outputs": {
+ "siteId": {
+ "type": "string",
+ "value": "[resourceId(parameters('resourceGroupName'), 'Microsoft.Web/sites', parameters('siteName'))]"
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/Templates/BasicAppServices.json b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/Templates/BasicAppServices.json
new file mode 100644
index 0000000000..6eefe37051
--- /dev/null
+++ b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/Templates/BasicAppServices.json
@@ -0,0 +1,32 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "parameters": {
+ "siteName": {
+ "type": "string"
+ },
+ "hostingPlanName": {
+ "type": "string"
+ },
+ "resourceGroupName": {
+ "type": "string"
+ }
+ },
+ "resources": [
+ {
+ "apiVersion": "2015-08-01",
+ "name": "[parameters('siteName')]",
+ "type": "Microsoft.Web/sites",
+ "location": "West US 2",
+ "properties": {
+ "serverFarmId": "[resourceId(parameters('resourceGroupName'), 'Microsoft.Web/serverFarms', parameters('hostingPlanName'))]"
+ }
+ }
+ ],
+ "outputs": {
+ "siteId": {
+ "type": "string",
+ "value": "[resourceId(parameters('resourceGroupName'), 'Microsoft.Web/sites', parameters('siteName'))]"
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/TestCommand.cs b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/TestCommand.cs
new file mode 100644
index 0000000000..d45eb59366
--- /dev/null
+++ b/test/Microsoft.AspNetCore.AzureAppServices.FunctionalTests/TestCommand.cs
@@ -0,0 +1,249 @@
+// 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