diff --git a/AzureIntegration.sln b/AzureIntegration.sln index db46b9da9f..0954531e43 100644 --- a/AzureIntegration.sln +++ b/AzureIntegration.sln @@ -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} diff --git a/NuGetPackageVerifier.json b/NuGetPackageVerifier.json index 4e5da217e3..cf7178529c 100644 --- a/NuGetPackageVerifier.json +++ b/NuGetPackageVerifier.json @@ -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. diff --git a/build/dependencies.props b/build/dependencies.props index c2ede2cdee..95bacc4edd 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -33,6 +33,8 @@ 15.3.0 1.4.0 4.7.49 + 10.0.1 + 1.5.0 8.1.4 2.3.0 2.3.0 diff --git a/src/Microsoft.Extensions.ApplicationModelDetection/AppModelDetectionResult.cs b/src/Microsoft.Extensions.ApplicationModelDetection/AppModelDetectionResult.cs new file mode 100644 index 0000000000..96e53dc52d --- /dev/null +++ b/src/Microsoft.Extensions.ApplicationModelDetection/AppModelDetectionResult.cs @@ -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; } + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.ApplicationModelDetection/AppModelDetector.cs b/src/Microsoft.Extensions.ApplicationModelDetection/AppModelDetector.cs new file mode 100644 index 0000000000..e42a8f2918 --- /dev/null +++ b/src/Microsoft.Extensions.ApplicationModelDetection/AppModelDetector.cs @@ -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"; + + /// + /// 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 + /// + /// The application directory + /// The instance containing information about application + 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; + } + } + + /// + /// Search for Microsoft.AspNetCore.Hosting entry in deps.json and get it's version number + /// + 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().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; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.ApplicationModelDetection/Microsoft.Extensions.ApplicationModelDetection.csproj b/src/Microsoft.Extensions.ApplicationModelDetection/Microsoft.Extensions.ApplicationModelDetection.csproj new file mode 100644 index 0000000000..aae6b8bf52 --- /dev/null +++ b/src/Microsoft.Extensions.ApplicationModelDetection/Microsoft.Extensions.ApplicationModelDetection.csproj @@ -0,0 +1,15 @@ + + + ASP.NET Core integration with Azure AppServices. + netstandard2.0 + $(NoWarn);CS1591 + true + true + aspnetcore;azure;appservices + + + + + + + \ No newline at end of file diff --git a/src/Microsoft.Extensions.ApplicationModelDetection/RuntimeFramework.cs b/src/Microsoft.Extensions.ApplicationModelDetection/RuntimeFramework.cs new file mode 100644 index 0000000000..b182c79eec --- /dev/null +++ b/src/Microsoft.Extensions.ApplicationModelDetection/RuntimeFramework.cs @@ -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 + } +} \ No newline at end of file diff --git a/test/Microsoft.Extensions.ApplicationModelDetection.Tests/AppModelTests.cs b/test/Microsoft.Extensions.ApplicationModelDetection.Tests/AppModelTests.cs new file mode 100644 index 0000000000..02e420d2e4 --- /dev/null +++ b/test/Microsoft.Extensions.ApplicationModelDetection.Tests/AppModelTests.cs @@ -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 = @""; + + [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 $@" + + + + + + + + +"; + } + + 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; + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.Extensions.ApplicationModelDetection.Tests/Microsoft.Extensions.ApplicationModelDetection.Tests.csproj b/test/Microsoft.Extensions.ApplicationModelDetection.Tests/Microsoft.Extensions.ApplicationModelDetection.Tests.csproj new file mode 100644 index 0000000000..b68825fcc0 --- /dev/null +++ b/test/Microsoft.Extensions.ApplicationModelDetection.Tests/Microsoft.Extensions.ApplicationModelDetection.Tests.csproj @@ -0,0 +1,12 @@ + + + + netcoreapp2.0;net461 + netcoreapp2.0 + + + + + + + \ No newline at end of file