Add application model detection package (#119)

This commit is contained in:
Pavel Krymets 2017-11-30 15:00:03 -08:00 committed by GitHub
parent 5b0cf49d0d
commit db6c04818e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 549 additions and 2 deletions

View File

@ -1,4 +1,4 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27004.2002
MinimumVisualStudioVersion = 15.0.26730.03
@ -50,6 +50,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Web.Xdt.Extension
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.AzureAppServices.SiteExtension.Tests", "test\Microsoft.AspNetCore.AzureAppServices.SiteExtension.Tests\Microsoft.AspNetCore.AzureAppServices.SiteExtension.Tests.csproj", "{491A857A-3529-4375-985D-D748F9F01476}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Extensions.ApplicationModelDetection", "src\Microsoft.Extensions.ApplicationModelDetection\Microsoft.Extensions.ApplicationModelDetection.csproj", "{F0CABFE8-A5B1-487B-A451-A486D26742D3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Extensions.ApplicationModelDetection.Tests", "test\Microsoft.Extensions.ApplicationModelDetection.Tests\Microsoft.Extensions.ApplicationModelDetection.Tests.csproj", "{15664836-2B94-4D2D-AC18-6DED01FCCCBD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -112,6 +116,14 @@ Global
{491A857A-3529-4375-985D-D748F9F01476}.Debug|Any CPU.Build.0 = Debug|Any CPU
{491A857A-3529-4375-985D-D748F9F01476}.Release|Any CPU.ActiveCfg = Release|Any CPU
{491A857A-3529-4375-985D-D748F9F01476}.Release|Any CPU.Build.0 = Release|Any CPU
{F0CABFE8-A5B1-487B-A451-A486D26742D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F0CABFE8-A5B1-487B-A451-A486D26742D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F0CABFE8-A5B1-487B-A451-A486D26742D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F0CABFE8-A5B1-487B-A451-A486D26742D3}.Release|Any CPU.Build.0 = Release|Any CPU
{15664836-2B94-4D2D-AC18-6DED01FCCCBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{15664836-2B94-4D2D-AC18-6DED01FCCCBD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{15664836-2B94-4D2D-AC18-6DED01FCCCBD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{15664836-2B94-4D2D-AC18-6DED01FCCCBD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -131,6 +143,8 @@ Global
{1EC31DA1-131D-4257-B001-BE8391E6077E} = {FF9B744E-6C59-40CC-9E41-9D2EBD292435}
{809AEE05-1B28-4E31-8959-776B249BD725} = {CD650B4B-81C2-4A44-AEF2-A251A877C1F0}
{491A857A-3529-4375-985D-D748F9F01476} = {CD650B4B-81C2-4A44-AEF2-A251A877C1F0}
{F0CABFE8-A5B1-487B-A451-A486D26742D3} = {FF9B744E-6C59-40CC-9E41-9D2EBD292435}
{15664836-2B94-4D2D-AC18-6DED01FCCCBD} = {CD650B4B-81C2-4A44-AEF2-A251A877C1F0}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5743DFE7-1AA5-439D-84AE-A480EA389927}

View File

@ -4,7 +4,8 @@
"AdxVerificationCompositeRule"
],
"packages": {
"Microsoft.AspNetCore.AzureAppServicesIntegration": { }
"Microsoft.AspNetCore.AzureAppServicesIntegration": { },
"Microsoft.Extensions.ApplicationModelDetection": { }
}
},
"Default": { // Rules to run for packages not listed in any other set.

View File

@ -33,6 +33,8 @@
<MicrosoftNETTestSdkPackageVersion>15.3.0</MicrosoftNETTestSdkPackageVersion>
<MicrosoftWebXdtPackageVersion>1.4.0</MicrosoftWebXdtPackageVersion>
<MoqPackageVersion>4.7.49</MoqPackageVersion>
<NewtonsoftJsonPackageVersion>10.0.1</NewtonsoftJsonPackageVersion>
<SystemReflectionMetadataPackageVersion>1.5.0</SystemReflectionMetadataPackageVersion>
<WindowsAzureStoragePackageVersion>8.1.4</WindowsAzureStoragePackageVersion>
<XunitPackageVersion>2.3.0</XunitPackageVersion>
<XunitRunnerVisualStudioPackageVersion>2.3.0</XunitRunnerVisualStudioPackageVersion>

View File

@ -0,0 +1,12 @@
// 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.
namespace Microsoft.Extensions.ApplicationModelDetection
{
public class AppModelDetectionResult
{
public RuntimeFramework? Framework { get; set; }
public string FrameworkVersion { get; set; }
public string AspNetCoreVersion { get; set; }
}
}

View File

@ -0,0 +1,250 @@
// 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.IO;
using System.Linq;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Xml.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Microsoft.Extensions.ApplicationModelDetection
{
public class AppModelDetector
{
// We use Hosting package to detect AspNetCore version
// it contains light-up implemenation that we care about and
// would have to be used by aspnet core web apps
private const string AspNetCoreAssembly = "Microsoft.AspNetCore.Hosting";
/// <summary>
/// Reads the following sources
/// - web.config to detect dotnet framework kind
/// - *.runtimeconfig.json to detect target framework version
/// - *.deps.json to detect Asp.Net Core version
/// - Microsoft.AspNetCore.Hosting.dll to detect Asp.Net Core version
/// </summary>
/// <param name="directory">The application directory</param>
/// <returns>The <see cref="AppModelDetectionResult"/> instance containing information about application</returns>
public AppModelDetectionResult Detect(DirectoryInfo directory)
{
string entryPoint = null;
// Try reading web.config and resolving framework and app path
var webConfig = directory.GetFiles("web.config").FirstOrDefault();
bool webConfigExists = webConfig != null;
bool? usesDotnetExe = null;
if (webConfigExists &&
TryParseWebConfig(webConfig, out var dotnetExe, out entryPoint))
{
usesDotnetExe = dotnetExe;
}
// If we found entry point let's look for .deps.json
// in some cases it exists in desktop too
FileInfo depsJson = null;
FileInfo runtimeConfig = null;
if (!string.IsNullOrWhiteSpace(entryPoint))
{
depsJson = new FileInfo(Path.ChangeExtension(entryPoint, ".deps.json"));
runtimeConfig = new FileInfo(Path.ChangeExtension(entryPoint, ".runtimeconfig.json"));
}
if (depsJson == null || !depsJson.Exists)
{
depsJson = directory.GetFiles("*.deps.json").FirstOrDefault();
}
if (runtimeConfig == null || !runtimeConfig.Exists)
{
runtimeConfig = directory.GetFiles("*.runtimeconfig.json").FirstOrDefault();
}
string aspNetCoreVersionFromDeps = null;
string aspNetCoreVersionFromDll = null;
// Try to detect ASP.NET Core version from .deps.json
if (depsJson != null &&
depsJson.Exists &&
TryParseDependencies(depsJson, out var aspNetCoreVersion))
{
aspNetCoreVersionFromDeps = aspNetCoreVersion;
}
// Try to detect ASP.NET Core version from .deps.json
var aspNetCoreDll = directory.GetFiles(AspNetCoreAssembly + ".dll").FirstOrDefault();
if (aspNetCoreDll != null &&
TryParseAssembly(aspNetCoreDll, out aspNetCoreVersion))
{
aspNetCoreVersionFromDll = aspNetCoreVersion;
}
// Try to detect dotnet core runtime version from runtimeconfig.json
string runtimeVersionFromRuntimeConfig = null;
if (runtimeConfig != null &&
runtimeConfig.Exists)
{
TryParseRuntimeConfig(runtimeConfig, out runtimeVersionFromRuntimeConfig);
}
var result = new AppModelDetectionResult();
if (usesDotnetExe == true)
{
result.Framework = RuntimeFramework.DotNetCore;
result.FrameworkVersion = runtimeVersionFromRuntimeConfig;
}
else
{
if (depsJson?.Exists == true &&
runtimeConfig?.Exists == true)
{
result.Framework = RuntimeFramework.DotNetCoreStandalone;
}
else
{
result.Framework = RuntimeFramework.DotNetFramework;
}
}
result.AspNetCoreVersion = aspNetCoreVersionFromDeps ?? aspNetCoreVersionFromDll;
return result;
}
private bool TryParseAssembly(FileInfo aspNetCoreDll, out string aspNetCoreVersion)
{
aspNetCoreVersion = null;
try
{
using (var stream = aspNetCoreDll.OpenRead())
using (var peReader = new PEReader(stream))
{
var metadataReader = peReader.GetMetadataReader();
var assemblyDefinition = metadataReader.GetAssemblyDefinition();
aspNetCoreVersion = assemblyDefinition.Version.ToString();
return true;
}
}
catch (Exception)
{
return false;
}
}
/// <summary>
/// Search for Microsoft.AspNetCore.Hosting entry in deps.json and get it's version number
/// </summary>
private bool TryParseDependencies(FileInfo depsJson, out string aspnetCoreVersion)
{
aspnetCoreVersion = null;
try
{
using (var streamReader = depsJson.OpenText())
using (var jsonReader = new JsonTextReader(streamReader))
{
var json = JObject.Load(jsonReader);
var libraryPrefix = AspNetCoreAssembly+ "/";
var library = json.Descendants().OfType<JProperty>().FirstOrDefault(property => property.Name.StartsWith(libraryPrefix));
if (library != null)
{
aspnetCoreVersion = library.Name.Substring(libraryPrefix.Length);
return true;
}
}
}
catch (Exception)
{
}
return false;
}
private bool TryParseRuntimeConfig(FileInfo runtimeConfig, out string frameworkVersion)
{
frameworkVersion = null;
try
{
using (var streamReader = runtimeConfig.OpenText())
using (var jsonReader = new JsonTextReader(streamReader))
{
var json = JObject.Load(jsonReader);
frameworkVersion = (string)json?["runtimeOptions"]
?["framework"]
?["version"];
return true;
}
}
catch (Exception)
{
return false;
}
}
private bool TryParseWebConfig(FileInfo webConfig, out bool usesDotnetExe, out string entryPoint)
{
usesDotnetExe = false;
entryPoint = null;
try
{
var xdocument = XDocument.Load(webConfig.FullName);
var aspNetCoreHandler = xdocument.Root?
.Element("system.webServer")
.Element("aspNetCore");
if (aspNetCoreHandler == null)
{
return false;
}
var processPath = (string) aspNetCoreHandler.Attribute("processPath");
var arguments = (string) aspNetCoreHandler.Attribute("arguments");
if (processPath.EndsWith("dotnet", StringComparison.OrdinalIgnoreCase) ||
processPath.EndsWith("dotnet.exe", StringComparison.OrdinalIgnoreCase) &&
!string.IsNullOrWhiteSpace(arguments))
{
usesDotnetExe = true;
var entryPointPart = arguments.Split(new[] {" "}, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
if (!string.IsNullOrWhiteSpace(entryPointPart))
{
try
{
entryPoint = Path.GetFullPath(Path.Combine(webConfig.DirectoryName, entryPointPart));
}
catch (Exception)
{
}
}
}
else
{
usesDotnetExe = false;
try
{
entryPoint = Path.GetFullPath(Path.Combine(webConfig.DirectoryName, processPath));
}
catch (Exception)
{
}
}
}
catch (Exception)
{
return false;
}
return true;
}
}
}

View File

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>ASP.NET Core integration with Azure AppServices.</Description>
<TargetFramework>netstandard2.0</TargetFramework>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>aspnetcore;azure;appservices</PackageTags>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="$(MicrosoftAspNetCoreHostingPackageVersion)" />
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonPackageVersion)" />
<PackageReference Include="System.Reflection.Metadata" Version="$(SystemReflectionMetadataPackageVersion)" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,12 @@
// 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.
namespace Microsoft.Extensions.ApplicationModelDetection
{
public enum RuntimeFramework
{
DotNetCore,
DotNetCoreStandalone,
DotNetFramework
}
}

View File

@ -0,0 +1,229 @@
// // 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.IO;
using Microsoft.AspNetCore.Hosting;
using Xunit;
namespace Microsoft.Extensions.ApplicationModelDetection.Tests
{
public class AppModelTests
{
private const string EmptyWebConfig = @"<?xml version=""1.0"" encoding=""utf-8""?><configuration></configuration>";
[Theory]
[InlineData("dotnet")]
[InlineData("dotnet.exe")]
[InlineData("%HOME%/dotnet")]
[InlineData("%HOME%/dotnet.exe")]
[InlineData("DoTNeT.ExE")]
public void DetectsCoreFrameworkFromWebConfig(string processPath)
{
using (var temp = new TemporaryDirectory()
.WithFile("web.config",GenerateWebConfig(processPath, ".\\app.dll")))
{
var result = new AppModelDetector().Detect(temp.Directory);
Assert.Equal(RuntimeFramework.DotNetCore, result.Framework);
}
}
[Theory]
[InlineData("app")]
[InlineData("app.exe")]
[InlineData("%HOME%/app")]
[InlineData("%HOME%/app.exe")]
public void DetectsFullFrameworkFromWebConfig(string processPath)
{
using (var temp = new TemporaryDirectory()
.WithFile("web.config", GenerateWebConfig(processPath, ".\\app.dll")))
{
var result = new AppModelDetector().Detect(temp.Directory);
Assert.Equal(RuntimeFramework.DotNetFramework, result.Framework);
}
}
[Theory]
[InlineData("2.0.0")]
[InlineData("2.0.0-preview1")]
[InlineData("1.1.3")]
public void DetectsRuntimeVersionFromRuntimeConfig(string runtimeVersion)
{
using (var temp = new TemporaryDirectory()
.WithFile("web.config", GenerateWebConfig("dotnet", ".\\app.dll"))
.WithFile("app.runtimeconfig.json", @"{
""runtimeOptions"": {
""tfm"": ""netcoreapp2.0"",
""framework"": {
""name"": ""Microsoft.NETCore.App"",
""version"": """+ runtimeVersion + @"""
},
""configProperties"": {
""System.GC.Server"": true
}
}
}"))
{
var result = new AppModelDetector().Detect(temp.Directory);
Assert.Equal(RuntimeFramework.DotNetCore, result.Framework);
Assert.Equal(runtimeVersion, result.FrameworkVersion);
}
}
[Theory]
[InlineData("2.0.0")]
[InlineData("2.0.0-preview1")]
[InlineData("1.1.3")]
public void DetectsRuntimeVersionFromRuntimeConfigWitoutEntryPoint(string runtimeVersion)
{
using (var temp = new TemporaryDirectory()
.WithFile("web.config", GenerateWebConfig("dotnet", "%HOME%\\app.dll"))
.WithFile("app.runtimeconfig.json", @"{
""runtimeOptions"": {
""tfm"": ""netcoreapp2.0"",
""framework"": {
""name"": ""Microsoft.NETCore.App"",
""version"": """+ runtimeVersion + @"""
},
""configProperties"": {
""System.GC.Server"": true
}
}
}"))
{
var result = new AppModelDetector().Detect(temp.Directory);
Assert.Equal(RuntimeFramework.DotNetCore, result.Framework);
Assert.Equal(runtimeVersion, result.FrameworkVersion);
}
}
[Theory]
[InlineData("2.0.0")]
[InlineData("2.0.0-preview1")]
[InlineData("1.1.3")]
public void DetectsAspNetCoreVersionFromDepsFile(string runtimeVersion)
{
using (var temp = new TemporaryDirectory()
.WithFile("web.config", GenerateWebConfig("dotnet", "app.dll"))
.WithFile("app.deps.json", @"{
""targets"": {
"".NETCoreApp,Version=v2.7"": {
""Microsoft.AspNetCore.Hosting/" + runtimeVersion + @""": { }
}
}
}"))
{
var result = new AppModelDetector().Detect(temp.Directory);
Assert.Equal(RuntimeFramework.DotNetCore, result.Framework);
Assert.Equal(runtimeVersion, result.AspNetCoreVersion);
}
}
[Theory]
[InlineData("2.0.0")]
[InlineData("2.0.0-preview1")]
[InlineData("1.1.3")]
public void DetectsAspNetCoreVersionFromDepsFileWithoutEntryPoint(string runtimeVersion)
{
using (var temp = new TemporaryDirectory()
.WithFile("web.config", GenerateWebConfig("dotnet", "%HOME%\\app.dll"))
.WithFile("app.deps.json", @"{
""targets"": {
"".NETCoreApp,Version=v2.7"": {
""Microsoft.AspNetCore.Hosting/" + runtimeVersion + @""": { }
}
}
}"))
{
var result = new AppModelDetector().Detect(temp.Directory);
Assert.Equal(RuntimeFramework.DotNetCore, result.Framework);
Assert.Equal(runtimeVersion, result.AspNetCoreVersion);
}
}
[Fact]
public void DetectsFullFrameworkWhenWebConfigExists()
{
using (var temp = new TemporaryDirectory()
.WithFile("web.config", EmptyWebConfig))
{
var result = new AppModelDetector().Detect(temp.Directory);
Assert.Equal(RuntimeFramework.DotNetFramework, result.Framework);
}
}
[Fact]
public void DetectsStandalone_WhenBothDepsAndRuntimeConfigExist()
{
using (var temp = new TemporaryDirectory()
.WithFile("web.config", GenerateWebConfig("app.exe", ""))
.WithFile("app.runtimeconfig.json", "{}")
.WithFile("app.deps.json", "{}"))
{
var result = new AppModelDetector().Detect(temp.Directory);
Assert.Equal(RuntimeFramework.DotNetCoreStandalone, result.Framework);
}
}
[Fact]
public void DetectsAspNetCoreVersionFromHostingDll()
{
using (var temp = new TemporaryDirectory()
.WithFile(typeof(WebHostBuilder).Assembly.Location))
{
var result = new AppModelDetector().Detect(temp.Directory);
Assert.Equal(typeof(WebHostBuilder).Assembly.GetName().Version.ToString(), result.AspNetCoreVersion);
}
}
private static string GenerateWebConfig(string processPath, string arguments)
{
return $@"<?xml version=""1.0"" encoding=""utf-8""?>
<configuration>
<system.webServer>
<handlers>
<add name=""aspNetCore"" path=""*"" verb=""*"" modules=""AspNetCoreModule"" resourceType=""Unspecified"" />
</handlers>
<aspNetCore processPath=""{processPath}"" arguments=""{arguments}"" stdoutLogEnabled=""false"" stdoutLogFile="".\logs\stdout"" />
</system.webServer>
</configuration>
";
}
private class TemporaryDirectory: IDisposable
{
public TemporaryDirectory()
{
Directory = new DirectoryInfo(Path.GetTempPath())
.CreateSubdirectory(Guid.NewGuid().ToString("N"));
}
public DirectoryInfo Directory { get; }
public void Dispose()
{
try
{
Directory.Delete(true);
}
catch (IOException)
{
}
}
public TemporaryDirectory WithFile(string name, string value)
{
File.WriteAllText(Path.Combine(Directory.FullName, name), value);
return this;
}
public TemporaryDirectory WithFile(string name)
{
File.Copy(name, Path.Combine(Directory.FullName, Path.GetFileName(name)));
return this;
}
}
}
}

View File

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">netcoreapp2.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Extensions.ApplicationModelDetection\Microsoft.Extensions.ApplicationModelDetection.csproj" />
</ItemGroup>
</Project>