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