File: ApplicationPublisher.cs
Web Access
Project: src\src\Hosting\Server.IntegrationTesting\src\Microsoft.AspNetCore.Server.IntegrationTesting.csproj (Microsoft.AspNetCore.Server.IntegrationTesting)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using Microsoft.Extensions.Logging;
 
namespace Microsoft.AspNetCore.Server.IntegrationTesting;
 
public class ApplicationPublisher
{
    public string ApplicationPath { get; }
 
    public ApplicationPublisher(string applicationPath)
    {
        ApplicationPath = applicationPath;
    }
 
    public static readonly string DotnetCommandName = "dotnet";
 
    public virtual Task<PublishedApplication> Publish(DeploymentParameters deploymentParameters, ILogger logger)
    {
        var publishDirectory = CreateTempDirectory();
        using (logger.BeginScope("dotnet-publish"))
        {
            if (string.IsNullOrEmpty(deploymentParameters.TargetFramework))
            {
                throw new Exception($"A target framework must be specified in the deployment parameters for applications that require publishing before deployment");
            }
 
            var parameters = $"publish "
                             + $" --output \"{publishDirectory.FullName}\""
                             + $" --framework {deploymentParameters.TargetFramework}"
                             + $" --configuration {deploymentParameters.Configuration}"
                             // avoids triggering builds of dependencies of the test app which could cause issues like https://github.com/dotnet/arcade/issues/2941
                             + $" --no-dependencies"
                             + $" /p:TargetArchitecture={deploymentParameters.RuntimeArchitecture}"
                             + (deploymentParameters.RestoreDependencies ? "" : " --no-restore");
 
            if (deploymentParameters.ApplicationType == ApplicationType.Standalone)
            {
                parameters += $" --self-contained --runtime {GetRuntimeIdentifier(deploymentParameters)}";
            }
            else
            {
                // Workaround for https://github.com/aspnet/websdk/issues/422
                parameters += " -p:UseAppHost=false";
            }
 
            parameters += $" {deploymentParameters.AdditionalPublishParameters}";
 
            var startInfo = new ProcessStartInfo
            {
                FileName = DotnetCommandName,
                Arguments = parameters,
                UseShellExecute = false,
                CreateNoWindow = true,
                RedirectStandardError = true,
                RedirectStandardOutput = true,
                WorkingDirectory = deploymentParameters.ApplicationPath,
            };
 
            ProcessHelpers.AddEnvironmentVariablesToProcess(startInfo, deploymentParameters.PublishEnvironmentVariables, logger);
 
            var hostProcess = new Process() { StartInfo = startInfo };
 
            logger.LogInformation($"Executing command {DotnetCommandName} {parameters}");
 
            hostProcess.StartAndCaptureOutAndErrToLogger("dotnet-publish", logger);
 
            // A timeout is passed to Process.WaitForExit() for two reasons:
            //
            // 1. When process output is read asynchronously, WaitForExit() without a timeout blocks until child processes
            //    are killed, which can cause hangs due to MSBuild NodeReuse child processes started by dotnet.exe.
            //    With a timeout, WaitForExit() returns when the parent process is killed and ignores child processes.
            //    https://stackoverflow.com/a/37983587/102052
            //
            // 2. If "dotnet publish" does hang indefinitely for some reason, tests should fail fast with an error message.
            var timeout = deploymentParameters.PublishTimeout ?? TimeSpan.FromMinutes(5);
 
            if (hostProcess.WaitForExit(milliseconds: (int)timeout.TotalMilliseconds))
            {
                if (hostProcess.ExitCode != 0)
                {
                    var message = $"{DotnetCommandName} publish exited with exit code : {hostProcess.ExitCode}";
                    logger.LogError(message);
                    throw new Exception(message);
                }
            }
            else
            {
                var message = $"{DotnetCommandName} publish failed to exit after {timeout.TotalMinutes} minutes";
                logger.LogError(message);
                throw new Exception(message);
            }
 
            logger.LogInformation($"{DotnetCommandName} publish finished with exit code : {hostProcess.ExitCode}");
        }
 
        return Task.FromResult(new PublishedApplication(publishDirectory.FullName, logger));
    }
 
    private static string GetRuntimeIdentifier(DeploymentParameters deploymentParameters)
    {
        var architecture = deploymentParameters.RuntimeArchitecture;
        if (OperatingSystem.IsWindows())
        {
            return "win-" + architecture;
        }
        if (OperatingSystem.IsLinux())
        {
            return "linux-" + architecture;
        }
        if (OperatingSystem.IsMacOS())
        {
            return "osx-" + architecture;
        }
        throw new InvalidOperationException("Unrecognized operation system platform");
    }
 
    protected static DirectoryInfo CreateTempDirectory()
    {
        var tempPath = Path.GetTempPath() + Guid.NewGuid().ToString("N");
        var target = new DirectoryInfo(tempPath);
        target.Create();
        return target;
    }
}