diff --git a/NuGetPackageVerifier.json b/NuGetPackageVerifier.json index 88e7842b53..d836176437 100644 --- a/NuGetPackageVerifier.json +++ b/NuGetPackageVerifier.json @@ -4,7 +4,8 @@ "AdxVerificationCompositeRule" ], "packages": { - "Microsoft.AspNetCore.Server.IISIntegration": { } + "Microsoft.AspNetCore.Server.IISIntegration": { }, + "Microsoft.AspNetCore.Server.IISIntegration.Tools": { } } }, "Default": { // Rules to run for packages not listed in any other set. diff --git a/src/Microsoft.AspNetCore.Server.IISIntegration.Tools/Internal/AnsiColorExtensions.cs b/src/Microsoft.AspNetCore.Server.IISIntegration.Tools/Internal/AnsiColorExtensions.cs new file mode 100755 index 0000000000..28ff11db48 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.IISIntegration.Tools/Internal/AnsiColorExtensions.cs @@ -0,0 +1,18 @@ +// 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. + +namespace Microsoft.Extensions.Cli.Utils +{ + public static class AnsiColorExtensions + { + public static string Red(this string text) + { + return "\x1B[31m" + text + "\x1B[39m"; + } + + public static string Yellow(this string text) + { + return "\x1B[33m" + text + "\x1B[39m"; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Server.IISIntegration.Tools/Internal/AnsiConsole.cs b/src/Microsoft.AspNetCore.Server.IISIntegration.Tools/Internal/AnsiConsole.cs new file mode 100755 index 0000000000..1ea22a6cd0 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.IISIntegration.Tools/Internal/AnsiConsole.cs @@ -0,0 +1,145 @@ +// 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.IO; + +namespace Microsoft.Extensions.Cli.Utils +{ + public class AnsiConsole + { + private AnsiConsole(TextWriter writer) + { + Writer = writer; + + OriginalForegroundColor = Console.ForegroundColor; + } + + private int _boldRecursion; + + public static AnsiConsole GetOutput() + { + return new AnsiConsole(Console.Out); + } + + public static AnsiConsole GetError() + { + return new AnsiConsole(Console.Error); + } + + public TextWriter Writer { get; } + + public ConsoleColor OriginalForegroundColor { get; } + + private void SetColor(ConsoleColor color) + { + const int Light = 0x08; + int c = (int)color; + + Console.ForegroundColor = + c < 0 ? color : // unknown, just use it + _boldRecursion > 0 ? (ConsoleColor)(c | Light) : // ensure color is light + (ConsoleColor)(c & ~Light); // ensure color is dark + } + + private void SetBold(bool bold) + { + _boldRecursion += bold ? 1 : -1; + if (_boldRecursion > 1 || (_boldRecursion == 1 && !bold)) + { + return; + } + + // switches on _boldRecursion to handle boldness + SetColor(Console.ForegroundColor); + } + + public void WriteLine(string message) + { + Write(message); + Writer.WriteLine(); + } + + + public void Write(string message) + { + var escapeScan = 0; + for (;;) + { + var escapeIndex = message.IndexOf("\x1b[", escapeScan, StringComparison.Ordinal); + if (escapeIndex == -1) + { + var text = message.Substring(escapeScan); + Writer.Write(text); + break; + } + else + { + var startIndex = escapeIndex + 2; + var endIndex = startIndex; + while (endIndex != message.Length && + message[endIndex] >= 0x20 && + message[endIndex] <= 0x3f) + { + endIndex += 1; + } + + var text = message.Substring(escapeScan, escapeIndex - escapeScan); + Writer.Write(text); + if (endIndex == message.Length) + { + break; + } + + switch (message[endIndex]) + { + case 'm': + int value; + if (int.TryParse(message.Substring(startIndex, endIndex - startIndex), out value)) + { + switch (value) + { + case 1: + SetBold(true); + break; + case 22: + SetBold(false); + break; + case 30: + SetColor(ConsoleColor.Black); + break; + case 31: + SetColor(ConsoleColor.Red); + break; + case 32: + SetColor(ConsoleColor.Green); + break; + case 33: + SetColor(ConsoleColor.Yellow); + break; + case 34: + SetColor(ConsoleColor.Blue); + break; + case 35: + SetColor(ConsoleColor.Magenta); + break; + case 36: + SetColor(ConsoleColor.Cyan); + break; + case 37: + SetColor(ConsoleColor.Gray); + break; + case 39: + Console.ForegroundColor = OriginalForegroundColor; + break; + } + } + break; + } + + escapeScan = endIndex + 1; + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Server.IISIntegration.Tools/Internal/Reporter.cs b/src/Microsoft.AspNetCore.Server.IISIntegration.Tools/Internal/Reporter.cs new file mode 100755 index 0000000000..be9e92bec3 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.IISIntegration.Tools/Internal/Reporter.cs @@ -0,0 +1,46 @@ +// 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. + +namespace Microsoft.Extensions.Cli.Utils +{ + // Stupid-simple console manager + public class Reporter + { + private static readonly Reporter NullReporter = new Reporter(console: null); + private static object _lock = new object(); + + private readonly AnsiConsole _console; + + private Reporter(AnsiConsole console) + { + _console = console; + } + + public static Reporter Output { get; } = new Reporter(AnsiConsole.GetOutput()); + public static Reporter Error { get; } = new Reporter(AnsiConsole.GetError()); + + public void WriteLine(string message) + { + lock (_lock) + { + _console?.WriteLine(message); + } + } + + public void WriteLine() + { + lock (_lock) + { + _console?.Writer?.WriteLine(); + } + } + + public void Write(string message) + { + lock (_lock) + { + _console?.Write(message); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Server.IISIntegration.Tools/Microsoft.AspNetCore.Server.IISIntegration.Tools.xproj b/src/Microsoft.AspNetCore.Server.IISIntegration.Tools/Microsoft.AspNetCore.Server.IISIntegration.Tools.xproj new file mode 100755 index 0000000000..2527791ac2 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.IISIntegration.Tools/Microsoft.AspNetCore.Server.IISIntegration.Tools.xproj @@ -0,0 +1,18 @@ + + + + 14.0.24720 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + b1bc61b7-ba1d-4100-a2e8-49d00ce2771d + Microsoft.AspNetCore.Server.IISIntegration.Tools + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\ + + + 2.0 + + + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Server.IISIntegration.Tools/Program.cs b/src/Microsoft.AspNetCore.Server.IISIntegration.Tools/Program.cs new file mode 100755 index 0000000000..f8acff10c0 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.IISIntegration.Tools/Program.cs @@ -0,0 +1,60 @@ +// 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 Microsoft.Extensions.Cli.Utils; +using Microsoft.Extensions.CommandLineUtils; + +namespace Microsoft.AspNetCore.Server.IISIntegration.Tools +{ + public class Program + { + public static int Main(string[] args) + { + var app = new CommandLineApplication + { + Name = "dotnet publish-iis", + FullName = "Asp.Net Core IIS Publisher", + Description = "IIS Publisher for the Asp.Net Core web applications", + }; + app.HelpOption("-h|--help"); + + var publishFolderOption = app.Option("-p|--publish-folder", "The path to the publish output folder", CommandOptionType.SingleValue); + var frameworkOption = app.Option("-f|--framework ", "Target framework of application being published", CommandOptionType.SingleValue); + var configurationOption = app.Option("-c|--configuration ", "Target configuration of application being published", CommandOptionType.SingleValue); + var projectPath = app.Argument("", "The path to the project (project folder or project.json) being published. If empty the current directory is used."); + + app.OnExecute(() => + { + var publishFolder = publishFolderOption.Value(); + var framework = frameworkOption.Value(); + + if (publishFolder == null || framework == null) + { + app.ShowHelp(); + return 2; + } + + Reporter.Output.WriteLine($"Configuring the following project for use with IIS: '{publishFolder}'"); + + var exitCode = new PublishIISCommand(publishFolder, framework, configurationOption.Value(), projectPath.Value).Run(); + + Reporter.Output.WriteLine("Configuring project completed successfully"); + + return exitCode; + }); + + try + { + return app.Execute(args); + } + catch (Exception e) + { + Reporter.Error.WriteLine(e.Message.Red()); + Reporter.Output.WriteLine(e.ToString().Yellow()); + } + + return 1; + } + } +} diff --git a/src/Microsoft.AspNetCore.Server.IISIntegration.Tools/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Server.IISIntegration.Tools/Properties/AssemblyInfo.cs new file mode 100755 index 0000000000..76feceeff0 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.IISIntegration.Tools/Properties/AssemblyInfo.cs @@ -0,0 +1,11 @@ +// 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.Reflection; +using System.Resources; + +[assembly: AssemblyMetadata("Serviceable", "True")] +[assembly: NeutralResourcesLanguage("en-us")] +[assembly: AssemblyCompany("Microsoft Corporation.")] +[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")] +[assembly: AssemblyProduct("Microsoft ASP.NET Core")] diff --git a/src/Microsoft.AspNetCore.Server.IISIntegration.Tools/PublishIISCommand.cs b/src/Microsoft.AspNetCore.Server.IISIntegration.Tools/PublishIISCommand.cs new file mode 100755 index 0000000000..b57b046821 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.IISIntegration.Tools/PublishIISCommand.cs @@ -0,0 +1,98 @@ +// 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.Xml; +using System.Xml.Linq; +using Microsoft.DotNet.ProjectModel; +using Microsoft.Extensions.Cli.Utils; +using NuGet.Frameworks; + +namespace Microsoft.AspNetCore.Server.IISIntegration.Tools +{ + public class PublishIISCommand + { + private readonly string _publishFolder; + private readonly string _projectPath; + private readonly string _framework; + private readonly string _configuration; + + public PublishIISCommand(string publishFolder, string framework, string configuration, string projectPath) + { + _publishFolder = publishFolder; + _projectPath = projectPath; + _framework = framework; + _configuration = configuration; + } + + public int Run() + { + var applicationBasePath = GetApplicationBasePath(); + + XDocument webConfigXml = null; + var webConfigPath = Path.Combine(_publishFolder, "web.config"); + if (File.Exists(webConfigPath)) + { + Reporter.Output.WriteLine($"Updating web.config at '{webConfigPath}'"); + + try + { + webConfigXml = XDocument.Load(webConfigPath); + } + catch (XmlException) { } + } + else + { + Reporter.Output.WriteLine($"No web.config found. Creating '{webConfigPath}'"); + } + + var projectContext = GetProjectContext(applicationBasePath, _framework); + var isPortable = !projectContext.TargetFramework.IsDesktop() && projectContext.IsPortable; + var applicationName = + projectContext.ProjectFile.GetCompilerOptions(projectContext.TargetFramework, _configuration).OutputName + + (isPortable ? ".dll" : ".exe"); + var transformedConfig = WebConfigTransform.Transform(webConfigXml, applicationName, ConfigureForAzure(), isPortable); + + using (var f = new FileStream(webConfigPath, FileMode.Create)) + { + transformedConfig.Save(f); + } + + return 0; + } + + private string GetApplicationBasePath() + { + if (!string.IsNullOrEmpty(_projectPath)) + { + var fullProjectPath = Path.GetFullPath(_projectPath); + + return Path.GetFileName(fullProjectPath) == "project.json" + ? Path.GetDirectoryName(fullProjectPath) + : fullProjectPath; + } + + return Directory.GetCurrentDirectory(); + } + + private static ProjectContext GetProjectContext(string applicationBasePath, string framework) + { + var project = ProjectReader.GetProject(Path.Combine(applicationBasePath, "project.json")); + + return new ProjectContextBuilder() + .WithProject(project) + .WithTargetFramework(framework) + .Build(); + } + + private static bool ConfigureForAzure() + { + var configureForAzureValue = Environment.GetEnvironmentVariable("DOTNET_CONFIGURE_AZURE"); + return string.Equals(configureForAzureValue, "true", StringComparison.Ordinal) || + string.Equals(configureForAzureValue, "1", StringComparison.Ordinal) || + !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WEBSITE_SITE_NAME")); + } + } +} diff --git a/src/Microsoft.AspNetCore.Server.IISIntegration.Tools/WebConfigTransform.cs b/src/Microsoft.AspNetCore.Server.IISIntegration.Tools/WebConfigTransform.cs new file mode 100755 index 0000000000..1db5cc8c8e --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.IISIntegration.Tools/WebConfigTransform.cs @@ -0,0 +1,135 @@ +// 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.Xml.Linq; + +namespace Microsoft.AspNetCore.Server.IISIntegration.Tools +{ + public static class WebConfigTransform + { + public static XDocument Transform(XDocument webConfig, string appName, bool configureForAzure, bool isPortable) + { + const string HandlersElementName = "handlers"; + const string aspNetCoreElementName = "aspNetCore"; + + webConfig = webConfig == null || webConfig.Root.Name.LocalName != "configuration" + ? XDocument.Parse("") + : webConfig; + + var webServerSection = GetOrCreateChild(webConfig.Root, "system.webServer"); + + TransformHandlers(GetOrCreateChild(webServerSection, HandlersElementName)); + TransformAspNetCore(GetOrCreateChild(webServerSection, aspNetCoreElementName), appName, configureForAzure, isPortable); + + // make sure that the aspNetCore element is after handlers element + var aspNetCoreElement = webServerSection.Element(HandlersElementName) + .ElementsBeforeSelf(aspNetCoreElementName).SingleOrDefault(); + if (aspNetCoreElement != null) + { + aspNetCoreElement.Remove(); + webServerSection.Element(HandlersElementName).AddAfterSelf(aspNetCoreElement); + } + + return webConfig; + } + + private static void TransformHandlers(XElement handlersElement) + { + var aspNetCoreElement = + handlersElement.Elements("add") + .FirstOrDefault(e => string.Equals((string)e.Attribute("name"), "aspnetcore", StringComparison.OrdinalIgnoreCase)); + + if (aspNetCoreElement == null) + { + aspNetCoreElement = new XElement("add"); + handlersElement.Add(aspNetCoreElement); + } + + aspNetCoreElement.SetAttributeValue("name", "aspNetCore"); + SetAttributeValueIfEmpty(aspNetCoreElement, "path", "*"); + SetAttributeValueIfEmpty(aspNetCoreElement, "verb", "*"); + SetAttributeValueIfEmpty(aspNetCoreElement, "modules", "AspNetCoreModule"); + SetAttributeValueIfEmpty(aspNetCoreElement, "resourceType", "Unspecified"); + } + + private static void TransformAspNetCore(XElement aspNetCoreElement, string appName, bool configureForAzure, bool isPortable) + { + // Forward slashes currently work neither in AspNetCoreModule nor in dotnet so they need to be + // replaced with backwards slashes when the application is published on a non-Windows machine + var appPath = Path.Combine(".", appName).Replace("/", "\\"); + RemoveLauncherArgs(aspNetCoreElement); + + if (!isPortable) + { + aspNetCoreElement.SetAttributeValue("processPath", appPath); + } + else + { + aspNetCoreElement.SetAttributeValue("processPath", "dotnet"); + + // In Xml the order of attributes does not matter but it is nice to have + // the `arguments` attribute next to the `processPath` attribute + var argumentsAttribute = aspNetCoreElement.Attribute("arguments"); + argumentsAttribute?.Remove(); + var attributes = aspNetCoreElement.Attributes().ToList(); + var processPathIndex = attributes.FindIndex(a => a.Name.LocalName == "processPath"); + attributes.Insert(processPathIndex + 1, + new XAttribute("arguments", (appPath + " " + (string)argumentsAttribute).Trim())); + + aspNetCoreElement.Attributes().Remove(); + aspNetCoreElement.Add(attributes); + } + + SetAttributeValueIfEmpty(aspNetCoreElement, "stdoutLogEnabled", "false"); + + var logPath = Path.Combine(configureForAzure ? @"\\?\%home%\LogFiles" : @".\logs", "stdout").Replace("/", "\\"); + if (configureForAzure) + { + // When publishing for Azure we want to always overwrite path - the folder we set the path to + // will exist, the path is not easy to customize and stdoutLogPath should be only used for + // diagnostic purposes + aspNetCoreElement.SetAttributeValue("stdoutLogFile", logPath); + } + else + { + SetAttributeValueIfEmpty(aspNetCoreElement, "stdoutLogFile", logPath); + } + } + + private static XElement GetOrCreateChild(XElement parent, string childName) + { + var childElement = parent.Element(childName); + if (childElement == null) + { + childElement = new XElement(childName); + parent.Add(childElement); + } + return childElement; + } + + private static void SetAttributeValueIfEmpty(XElement element, string attributeName, string value) + { + element.SetAttributeValue(attributeName, (string)element.Attribute(attributeName) ?? value); + } + + private static void RemoveLauncherArgs(XElement aspNetCoreElement) + { + var arguments = (string)aspNetCoreElement.Attribute("arguments"); + + if (arguments != null) + { + const string launcherArgs = "%LAUNCHER_ARGS%"; + var position = 0; + while ((position = arguments.IndexOf(launcherArgs, position, StringComparison.OrdinalIgnoreCase)) >= 0) + { + arguments = arguments.Remove(position, launcherArgs.Length); + } + + aspNetCoreElement.SetAttributeValue("arguments", arguments.Trim()); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Server.IISIntegration.Tools/project.json b/src/Microsoft.AspNetCore.Server.IISIntegration.Tools/project.json new file mode 100755 index 0000000000..3f65fb18c0 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.IISIntegration.Tools/project.json @@ -0,0 +1,39 @@ +{ + "version": "1.1.0-preview4-final", + "description": "IIS Integration publish tool for .NET Core CLI. Contains the dotnet-publish-iis command for publishing web applications to be hosted using IIS.", + "packOptions": { + "repository": { + "type": "git", + "url": "git://github.com/aspnet/IISIntegration" + }, + "tags": [ + "aspnetcore", + "iis" + ] + }, + "buildOptions": { + "emitEntryPoint": true, + "warningsAsErrors": true, + "keyFile": "../../tools/Key.snk", + "nowarn": [ + "CS1591" + ], + "xmlDoc": true, + "outputName": "dotnet-publish-iis" + }, + "dependencies": { + "Microsoft.Extensions.CommandLineUtils": "1.1.0-*", + "Microsoft.DotNet.ProjectModel": "1.0.0-rc3-003121", + "System.Diagnostics.Process": "4.3.0-*" + }, + "frameworks": { + "netcoreapp1.0": { + "dependencies": { + "Microsoft.NETCore.App": { + "type": "platform", + "version": "1.1.0-*" + } + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Server.IISIntegration.Tools.Tests/Microsoft.AspNetCore.Server.IISIntegration.Tools.Tests.xproj b/test/Microsoft.AspNetCore.Server.IISIntegration.Tools.Tests/Microsoft.AspNetCore.Server.IISIntegration.Tools.Tests.xproj new file mode 100755 index 0000000000..4d265e078f --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.IISIntegration.Tools.Tests/Microsoft.AspNetCore.Server.IISIntegration.Tools.Tests.xproj @@ -0,0 +1,21 @@ + + + + 14.0.24720 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + d0fa003d-de4c-480e-b4a4-bd38691b36ad + Microsoft.AspNetCore.Server.IISIntegration.Tools.Tests + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\ + + + 2.0 + + + + + + \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Server.IISIntegration.Tools.Tests/PublishIISCommandFacts.cs b/test/Microsoft.AspNetCore.Server.IISIntegration.Tools.Tests/PublishIISCommandFacts.cs new file mode 100755 index 0000000000..5131e7c74f --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.IISIntegration.Tools.Tests/PublishIISCommandFacts.cs @@ -0,0 +1,188 @@ +using Xunit; +using System.IO; +using System.Linq; +using System.Xml.Linq; +using System; + +namespace Microsoft.AspNetCore.Server.IISIntegration.Tools.Tests +{ + public class PublishIISCommandFacts + { + private class Folders + { + public string TestRoot; + public string PublishOutput; + public string ProjectPath; + } + + [Theory] + [InlineData("netcoreapp1.0")] + [InlineData("netstandard1.5")] + public void PublishIIS_uses_default_values_if_options_not_specified(string targetFramework) + { + var folders = CreateTestDir($@"{{ ""frameworks"": {{ ""{targetFramework}"": {{ }} }} }}"); + + new PublishIISCommand(folders.PublishOutput, targetFramework, null, folders.ProjectPath).Run(); + + var processPath = (string)GetPublishedWebConfig(folders.PublishOutput) + .Descendants("aspNetCore").Attributes("processPath").Single(); + + Assert.Equal(@".\projectDir.exe", processPath); + + Directory.Delete(folders.TestRoot, recursive: true); + } + + [Fact] + public void PublishIIS_can_publish_for_portable_app() + { + var folders = CreateTestDir( +@" + { + ""frameworks"": { + ""netcoreapp1.0"": { + ""dependencies"": { + ""Microsoft.NETCore.App"": { + ""version"": ""1.0.0-*"", + ""type"": ""platform"" + } + } + } + } + }"); + + new PublishIISCommand(folders.PublishOutput, "netcoreapp1.0", null, folders.ProjectPath).Run(); + + var aspNetCoreElement = GetPublishedWebConfig(folders.PublishOutput) + .Descendants("aspNetCore").Single(); + + Assert.Equal(@"dotnet", (string)aspNetCoreElement.Attribute("processPath")); + Assert.Equal(@".\projectDir.dll", (string)aspNetCoreElement.Attribute("arguments")); + + Directory.Delete(folders.TestRoot, recursive: true); + } + + [Theory] + [InlineData("awesomeApp")] + [InlineData("awesome.App")] + public void PublishIIS_reads_application_name_from_project_json_if_exists(string projectName) + { + var folders = CreateTestDir($@"{{ ""name"": ""{projectName}"", ""frameworks"": {{ ""netcoreapp1.0"": {{}} }} }}"); + + new PublishIISCommand(folders.PublishOutput, "netcoreapp1.0", null, folders.ProjectPath).Run(); + + var processPath = (string)GetPublishedWebConfig(folders.PublishOutput) + .Descendants("aspNetCore").Attributes("processPath").Single(); + + Assert.Equal($@".\{projectName}.exe", processPath); + + Directory.Delete(folders.TestRoot, recursive: true); + } + + [Fact] + public void PublishIIS_reads_application_name_from_outputName_if_specified() + { + var folders = CreateTestDir( +@"{ + ""name"": ""awesomeApp"", + ""buildOptions"": { ""outputName"": ""myApp"" }, + ""frameworks"": { ""netcoreapp1.0"": { } } +}"); + + new PublishIISCommand(folders.PublishOutput, "netcoreapp1.0", null, folders.ProjectPath).Run(); + + var processPath = (string)GetPublishedWebConfig(folders.PublishOutput) + .Descendants("aspNetCore").Attributes("processPath").Single(); + + Assert.Equal(@".\myApp.exe", processPath); + + Directory.Delete(folders.TestRoot, recursive: true); + } + + [Theory] + [InlineData("Debug", "myApp")] + [InlineData("Release", "awesomeApp")] + public void PublishIIS_reads_application_name_from_configuration_specific_outputName_if_specified(string configuration, string expectedName) + { + var folders = CreateTestDir( +@"{ + ""name"": ""awesomeApp"", + ""configurations"": { ""Debug"": { ""buildOptions"": { ""outputName"": ""myApp"" } } }, + ""frameworks"": { ""netcoreapp1.0"": { } } +}"); + + new PublishIISCommand(folders.PublishOutput, "netcoreapp1.0", configuration, folders.ProjectPath).Run(); + + var processPath = (string)GetPublishedWebConfig(folders.PublishOutput) + .Descendants("aspNetCore").Attributes("processPath").Single(); + + Assert.Equal($@".\{expectedName}.exe", processPath); + + Directory.Delete(folders.TestRoot, recursive: true); + } + + [Theory] + [InlineData("projectDir")] + [InlineData("project.Dir")] + public void PublishIIS_accepts_path_to_project_json_as_project_path(string projectDir) + { + var folders = CreateTestDir(@"{ ""frameworks"": { ""netcoreapp1.0"": { } } }", projectDir); + + new PublishIISCommand(folders.PublishOutput, "netcoreapp1.0", null, + Path.Combine(folders.ProjectPath, "project.json")).Run(); + + var processPath = (string)GetPublishedWebConfig(folders.PublishOutput) + .Descendants("aspNetCore").Attributes("processPath").Single(); + + Assert.Equal($@".\{projectDir}.exe", processPath); + + Directory.Delete(folders.TestRoot, recursive: true); + } + + [Fact] + public void PublishIIS_modifies_existing_web_config() + { + var folders = CreateTestDir(@"{ ""frameworks"": { ""netcoreapp1.0"": { } } }"); + + File.WriteAllText(Path.Combine(folders.PublishOutput, "web.config"), +@" + + + + + + +"); + + new PublishIISCommand(folders.PublishOutput, "netcoreapp1.0", null, + Path.Combine(folders.ProjectPath, "project.json")).Run(); + + var aspNetCoreElement = GetPublishedWebConfig(folders.PublishOutput) + .Descendants("aspNetCore").Single(); + + Assert.Equal(@".\projectDir.exe", (string)aspNetCoreElement.Attribute("processPath")); + Assert.Equal(@"1234", (string)aspNetCoreElement.Attribute("startupTimeLimit")); + + Directory.Delete(folders.TestRoot, recursive: true); + } + + private XDocument GetPublishedWebConfig(string publishOut) + { + return XDocument.Load(Path.Combine(publishOut, "web.config")); + } + + private Folders CreateTestDir(string projectJson, string projectDir = "projectDir") + { + var testRoot = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(testRoot); + + var projectPath = Path.Combine(testRoot, projectDir); + Directory.CreateDirectory(projectPath); + File.WriteAllText(Path.Combine(projectPath, "project.json"), projectJson); + + var publishOut = Path.Combine(testRoot, "publishOut"); + Directory.CreateDirectory(publishOut); + + return new Folders { TestRoot = testRoot, ProjectPath = projectPath, PublishOutput = publishOut }; + } + } +} diff --git a/test/Microsoft.AspNetCore.Server.IISIntegration.Tools.Tests/WebConfigTransformFacts.cs b/test/Microsoft.AspNetCore.Server.IISIntegration.Tools.Tests/WebConfigTransformFacts.cs new file mode 100755 index 0000000000..d163140260 --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.IISIntegration.Tools.Tests/WebConfigTransformFacts.cs @@ -0,0 +1,257 @@ +using Xunit; +using System.Linq; +using System.Xml.Linq; + +namespace Microsoft.AspNetCore.Server.IISIntegration.Tools.Tests +{ + public class WebConfigTransformFacts + { + private XDocument WebConfigTemplate => XDocument.Parse( +@" + + + + + + +"); + + [Fact] + public void WebConfigTransform_creates_new_config_if_one_does_not_exist() + { + Assert.True(XNode.DeepEquals(WebConfigTemplate, + WebConfigTransform.Transform(null, "test.exe", configureForAzure: false, isPortable: false))); + } + + [Fact] + public void WebConfigTransform_creates_new_config_if_one_has_unexpected_format() + { + Assert.True(XNode.DeepEquals(WebConfigTemplate, + WebConfigTransform.Transform(XDocument.Parse(""), "test.exe", configureForAzure: false, isPortable: false))); + } + + [Theory] + [InlineData(new object[] { new[] { "system.webServer" } })] + [InlineData(new object[] { new[] { "add" } })] + [InlineData(new object[] { new[] { "handlers" } })] + [InlineData(new object[] { new[] { "aspNetCore" } })] + [InlineData(new object[] { new[] { "environmentVariables" } })] + [InlineData(new object[] { new[] { "environmentVariable" } })] + [InlineData(new object[] { new[] { "handlers", "aspNetCore", "environmentVariables" } })] + public void WebConfigTransform_adds_missing_elements(string[] elementNames) + { + var input = WebConfigTemplate; + foreach (var elementName in elementNames) + { + input.Descendants(elementName).Remove(); + } + + Assert.True(XNode.DeepEquals(WebConfigTemplate, + WebConfigTransform.Transform(input, "test.exe", configureForAzure: false, isPortable: false))); + } + + [Theory] + [InlineData("add", "path", "test")] + [InlineData("add", "verb", "test")] + [InlineData("add", "modules", "mods")] + [InlineData("add", "resourceType", "Either")] + [InlineData("aspNetCore", "stdoutLogEnabled", "true")] + [InlineData("aspNetCore", "startupTimeLimit", "1200")] + [InlineData("aspNetCore", "arguments", "arg1")] + [InlineData("aspNetCore", "stdoutLogFile", "logfile")] + public void WebConfigTransform_wont_override_custom_values(string elementName, string attributeName, string attributeValue) + { + var input = WebConfigTemplate; + input.Descendants(elementName).Single().SetAttributeValue(attributeName, attributeValue); + + var output = WebConfigTransform.Transform(input, "test.exe", configureForAzure: false, isPortable: false); + Assert.Equal(attributeValue, (string)output.Descendants(elementName).Single().Attribute(attributeName)); + } + + [Fact] + public void WebConfigTransform_overwrites_processPath() + { + var newProcessPath = + (string)WebConfigTransform.Transform(WebConfigTemplate, "app.exe", configureForAzure: false, isPortable: false) + .Descendants("aspNetCore").Single().Attribute("processPath"); + + Assert.Equal(@".\app.exe", newProcessPath); + } + + [Fact] + public void WebConfigTransform_fixes_aspnetcore_casing() + { + var input = WebConfigTemplate; + input.Descendants("add").Single().SetAttributeValue("name", "aspnetcore"); + + Assert.True(XNode.DeepEquals(WebConfigTemplate, + WebConfigTransform.Transform(input, "test.exe", configureForAzure: false, isPortable: false))); + } + + [Fact] + public void WebConfigTransform_does_not_remove_children_of_aspNetCore_element() + { + var envVarElement = + new XElement("environmentVariable", new XAttribute("name", "ENVVAR"), new XAttribute("value", "123")); + + var input = WebConfigTemplate; + input.Descendants("aspNetCore").Single().Add(envVarElement); + + Assert.True(XNode.DeepEquals(envVarElement, + WebConfigTransform.Transform(input, "app.exe", configureForAzure: false, isPortable: false) + .Descendants("environmentVariable").SingleOrDefault(e => (string)e.Attribute("name") == "ENVVAR"))); + } + + [Fact] + public void WebConfigTransform_adds_stdoutLogEnabled_if_attribute_is_missing() + { + var input = WebConfigTemplate; + input.Descendants("aspNetCore").Attributes("stdoutLogEnabled").Remove(); + + Assert.Equal( + "false", + (string)WebConfigTransform.Transform(input, "test.exe", configureForAzure: false, isPortable: false) + .Descendants().Attributes("stdoutLogEnabled").Single()); + } + + [Theory] + [InlineData(null)] + [InlineData("false")] + [InlineData("true")] + public void WebConfigTransform_adds_stdoutLogFile_if_attribute_is_missing(string stdoutLogFile) + { + var input = WebConfigTemplate; + + var aspNetCoreElement = input.Descendants("aspNetCore").Single(); + aspNetCoreElement.Attribute("stdoutLogEnabled").Remove(); + if (stdoutLogFile != null) + { + aspNetCoreElement.SetAttributeValue("stdoutLogEnabled", stdoutLogFile); + } + + Assert.Equal( + @".\logs\stdout", + (string)WebConfigTransform.Transform(input, "test.exe", configureForAzure: false, isPortable: false) + .Descendants().Attributes("stdoutLogFile").Single()); + } + + [Theory] + [InlineData(null)] + [InlineData("true")] + [InlineData("false")] + public void WebConfigTransform_does_not_change_existing_stdoutLogEnabled(string stdoutLogEnabledValue) + { + var input = WebConfigTemplate; + var aspNetCoreElement = input.Descendants("aspNetCore").Single(); + + aspNetCoreElement.SetAttributeValue("stdoutLogFile", "mylog.txt"); + aspNetCoreElement.Attributes("stdoutLogEnabled").Remove(); + if (stdoutLogEnabledValue != null) + { + input.Descendants("aspNetCore").Single().SetAttributeValue("stdoutLogEnabled", stdoutLogEnabledValue); + } + + Assert.Equal( + "mylog.txt", + (string)WebConfigTransform.Transform(input, "test.exe", configureForAzure: false, isPortable: false) + .Descendants().Attributes("stdoutLogFile").Single()); + } + + [Fact] + public void WebConfigTransform_correctly_configures_for_Azure() + { + var input = WebConfigTemplate; + input.Descendants("aspNetCore").Attributes().Remove(); + + var aspNetCoreElement = WebConfigTransform.Transform(input, "test.exe", configureForAzure: true, isPortable: false) + .Descendants("aspNetCore").Single(); + aspNetCoreElement.Elements().Remove(); + + Assert.True(XNode.DeepEquals( + XDocument.Parse(@"").Root, + aspNetCoreElement)); + } + + [Fact] + public void WebConfigTransform_overwrites_stdoutLogPath_for_Azure() + { + var input = WebConfigTemplate; + var output = WebConfigTransform.Transform(input, "test.exe", configureForAzure: true, isPortable: false); + + Assert.Equal( + @"\\?\%home%\LogFiles\stdout", + (string)output.Descendants("aspNetCore").Single().Attribute("stdoutLogFile")); + } + + [Fact] + public void WebConfigTransform_configures_portable_apps_correctly() + { + var aspNetCoreElement = + WebConfigTransform.Transform(WebConfigTemplate, "test.exe", configureForAzure: false, isPortable: true) + .Descendants("aspNetCore").Single(); + + Assert.True(XNode.DeepEquals( + XDocument.Parse(@"").Root, + aspNetCoreElement)); + } + + [Theory] + [InlineData("%LAUNCHER_ARGS%", "")] + [InlineData(" %launcher_ARGS%", "")] + [InlineData("%LAUNCHER_args% ", "")] + [InlineData("%LAUNCHER_ARGS% %launcher_args%", "")] + [InlineData(" %LAUNCHER_ARGS% %launcher_args% ", "")] + [InlineData(" %launcher_args% -my-switch", "-my-switch")] + [InlineData("-my-switch %LaUnChEr_ArGs%", "-my-switch")] + [InlineData("-switch-1 %LAUNCHER_ARGS% -switch-2", "-switch-1 -switch-2")] + [InlineData("%LAUNCHER_ARGS% -switch %launcher_args%", "-switch")] + public void WebConfigTransform_removes_LAUNCHER_ARGS_from_arguments_for_standalone_apps(string inputArguments, string outputArguments) + { + var input = WebConfigTemplate; + input.Descendants("aspNetCore").Single().SetAttributeValue("arguments", inputArguments); + + var aspNetCoreElement = + WebConfigTransform.Transform(input, "test.exe", configureForAzure: false, isPortable: false) + .Descendants("aspNetCore").Single(); + + Assert.Equal(outputArguments, (string)aspNetCoreElement.Attribute("arguments")); + } + + [Theory] + [InlineData("", ".\\myapp.dll")] + [InlineData("%LAUNCHER_ARGS%", ".\\myapp.dll")] + [InlineData("%LAUNCHER_ARGS% %launcher_args%", ".\\myapp.dll")] + [InlineData("-my-switch", ".\\myapp.dll -my-switch")] + [InlineData(" %launcher_args% -my-switch", ".\\myapp.dll -my-switch")] + [InlineData("-my-switch %LaUnChEr_ArGs%", ".\\myapp.dll -my-switch")] + [InlineData("-switch-1 -switch-2", ".\\myapp.dll -switch-1 -switch-2")] + [InlineData("-switch-1 %LAUNCHER_ARGS% -switch-2", ".\\myapp.dll -switch-1 -switch-2")] + [InlineData("%LAUNCHER_ARGS% -switch %launcher_args%", ".\\myapp.dll -switch")] + public void WebConfigTransform_wont_override_existing_args_for_portable_apps(string inputArguments, string outputArguments) + { + var input = WebConfigTemplate; + input.Descendants("aspNetCore").Single().SetAttributeValue("arguments", inputArguments); + + var aspNetCoreElement = + WebConfigTransform.Transform(input, "myapp.dll", configureForAzure: false, isPortable: true) + .Descendants("aspNetCore").Single(); + + Assert.Equal(outputArguments, (string)aspNetCoreElement.Attribute("arguments")); + } + + + private bool VerifyMissingElementCreated(params string[] elementNames) + { + var input = WebConfigTemplate; + foreach (var elementName in elementNames) + { + input.Descendants(elementName).Remove(); + } + + return XNode.DeepEquals(WebConfigTemplate, + WebConfigTransform.Transform(input, "test.exe", configureForAzure: false, isPortable: false)); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Server.IISIntegration.Tools.Tests/project.json b/test/Microsoft.AspNetCore.Server.IISIntegration.Tools.Tests/project.json new file mode 100755 index 0000000000..5dec4b7858 --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.IISIntegration.Tools.Tests/project.json @@ -0,0 +1,24 @@ +{ + "dependencies": { + "dotnet-test-xunit": "1.0.0-rc3-000000-01", + "xunit": "2.1.0", + "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.1.0-preview4-final" + }, + "frameworks": { + "netcoreapp1.0": { + "imports": [ + "portable-dnxcore50+net45+win8+wp8+wpa81", + "dotnet", + "portable-net45+win8" + ], + "dependencies": { + "System.Diagnostics.Process": "4.3.0-*", + "Microsoft.NETCore.App": { + "type": "platform", + "version": "1.1.0-*" + } + } + } + }, + "testRunner": "xunit" +} \ No newline at end of file